Neste artigo, demonstramos como implementar o compartilhamento de tela entre duas aplicações Compose Desktop utilizando as capacidades do JxBrowser.
O JxBrowser é uma biblioteca JVM multiplataforma que permite integrar o controle do navegador Web baseado no Chromium em suas aplicações Compose, Swing, JavaFX, SWT, e utilizar centenas de funcionalidades do Chromium. Para implementar o compartilhamento de tela em Kotlin, aproveitamos o suporte WebRTC do Chromium e do acesso programático do JxBrowser a ele.
Visão geral
A WebRTC é uma norma aberta, que permite a comunicação em tempo real através de uma API JavaScript regular. A tecnologia está disponível em todos os navegadores modernos, assim como em clientes nativos para todas as principais plataformas. A utilizaremos para enviar o streaming de vídeo da tela capturada de uma aplicação para outra.
O projeto é composto por quatro módulos:
server
– um servidor de sinalização para estabelecer conexões multimídia diretas entre pares.sender
– uma aplicação Compose que compartilha a tela principal.receiver
– uma aplicação Compose que exibe a tela compartilhada.common
– contém um código comum compartilhado entre os módulos Kotlin.
O servidor de sinalização facilita a troca inicial de informações de conexão entre pares. Isso inclui informações sobre a rede, descritores de sessão, e capacidades multimídia.
Os clientes Compose são duas aplicações desktop: uma que compartilha a tela com um único clique e outra que recebe e apresenta o streaming de vídeo.
Servidor
Um dos principais desafios do WebRTC é a gestão da sinalização, que serve de ponto de encontro entre pares, sem transmitir dados efetivos. Utilizamos a biblioteca PeerJS que abstrai a lógica de sinalização, nos permitindo concentrar na funcionalidade da aplicação. A biblioteca fornece implementações de servidor e de cliente.
Tudo o que precisamos fazer é criar uma instância do PeerServer
e executar a
aplicação Node.js criada.
Para começar, adicionamos a dependência NPM necessária:
npm install peer
Em seguida, crie uma instância do PeerServer
:
PeerServer({
port: 3000
});
E execute a aplicação criada:
node server.js
Clientes Compose
Para aplicações Compose, precisamos inicializar um projeto Gradle vazio com JxBrowser e plugins Compose:
plugins {
id("org.jetbrains.kotlin.jvm") version "2.0.0"
id("com.teamdev.jxbrowser") version "1.2.1"
id("org.jetbrains.compose") version "1.6.11"
}
jxbrowser {
version = "8.2.1"
includePreviewBuilds()
}
dependencies {
implementation(jxbrowser.currentPlatform)
implementation(jxbrowser.compose)
implementation(compose.desktop.currentOs)
}
Cada cliente Compose é composto por três camadas:
- Camada UI – Compose’s
singleWindowApplication
, que exibe a interface. - Camada de integração do navegador – biblioteca JxBrowser, que incorpora o navegador na aplicação Compose.
- Componente WebRTC – biblioteca PeerJS, que estabelece conexões WebRTC e trata streaming multimídia para compartilhamento de tela.
Aplicação do receptor
Vamos começar pela aplicação do receptor que exibe a tela compartilhada.
Precisamos implementar um componente WebRTC de recepção. Deve conectar-se ao
servidor de sinalização e subscrever as chamadas recebidas. A sua API consistirá
numa única função connect()
. A função deve ser exposta ao âmbito global para
ser acessível a partir do lado Kotlin.
window.connect = (signalingServer) => {
const peer = new Peer(RECEIVER_PEER_ID, signalingServer);
peer.on('call', (call) => {
call.answer();
call.on('stream', (stream) => {
showVideo(stream);
});
call.on('close', () => {
hideVideo();
});
});
}
Respondemos a cada chamada e mostramos o fluxo recebido no elemento <video>
,
caso contrário o elemento fica oculto. Um link para o código-fonte completo no
GitHub é fornecido no final do artigo.
Em seguida, utilizaremos o JxBrowser para carregar este componente e tornar as funções JS expostas invocáveis a partir de Kotlin:
class WebrtcReceiver(browser: Browser) {
private const val webrtcComponent = "/receiving-peer.html"
private val frame = browser.mainFrame!!
init {
browser.loadWebPage(webrtcComponent)
}
fun connect(server: SignalingServer) = executeJavaScript("connect($server)")
// Executa o código JS fornecido no frame carregado.
private fun executeJavaScript(javaScript: String) = // ...
// Carrega a página a partir dos recursos da aplicação.
private fun Browser.loadWebPage(webPage: String) = // ...
}
O WebrtcReceiver
carrega o componente implementado a partir dos recursos da
aplicação para o browser e expõe um único método público connect(...)
, que
invoca diretamente a sua contraparte JS.
Por fim, vamos criar uma aplicação Compose para juntar todas as peças:
singleWindowApplication(title = "Screen Viewer") {
val engine = remember { createEngine() }
val browser = remember { engine.newBrowser() }
val webrtc = remember { WebrtcReceiver(browser) }
BrowserView(browser)
LaunchedEffect(Unit) {
webrtc.connect(SIGNALING_SERVER)
}
}
Primeiro, criamos as instâncias Engine
, Browser
e WebrtcReceiver
. Então,
nós adicionamos o composable BrowserView
para exibir o player de vídeo HTML5.
No efeito lançado, nos conectamos ao servidor de sinalização, supondo que se
trata de uma operação rápida e sem falhas.
Aplicação do remetente
Na aplicação do remetente, compartilhamos a tela principal fazendo uma chamada para o receptor.
É necessário implementar o componente WebRTC de envio. Ele deve ser capaz de se conectar ao servidor de sinalização, iniciar e parar uma sessão de compartilhamento de tela. Assim, a sua API será composta por três funções. Estas funções devem ser expostas ao âmbito global, para que sejam acessíveis a partir do lado Kotlin.
let peer;
let mediaConnection;
let mediaStream;
window.connect = (signalingServer) => {
peer = new Peer(SENDER_PEER_ID, signalingServer);
}
window.startScreenSharing = () => {
navigator.mediaDevices.getDisplayMedia({
video: {cursor: 'always'}
}).then(stream => {
mediaConnection = peer.call(RECEIVER_PEER_ID, stream);
mediaStream = stream;
});
}
window.stopScreenSharing = () => {
mediaConnection.close();
mediaStream.getTracks().forEach(track => track.stop());
}
Após estabelecida a conexão ao servidor de sinalização, o componente pode iniciar e parar uma sessão de compartilhamento de tela. Iniciar uma nova sessão implica escolher um fluxo multimídia com o qual chamar o receptor. Tanto o streaming como a conexão mídia P2P devem ser fechados quando compartilhamento é interrompido. A conexão ao servidor de sinalização é mantida ativa enquanto o componente for carregado.
De maneira similar da aplicação de recepção, vamos criar um wrapper Kotlin para
este componente. Vamos estender o WebrtcReceiver
com uma nova funcionalidade
para criar o WebrtcSender
.
Em primeiro lugar, precisamos dizer ao JxBrowser qual a fonte de vídeo a utilizar. Por padrão, quando uma página Web pretende capturar vídeo da tela, o Chromium apresenta uma caixa de diálogo onde podemos escolher uma fonte. Com a API JxBrowser, podemos especificar a fonte de captura diretamente no código:
init {
// Selecionar uma fonte quando o navegador está prestes a iniciar uma sessão de captura.
browser.register(StartCaptureSessionCallback { params: Params, tell: Action ->
val primaryScreen = params.sources().screens()[0]
tell.selectSource(primaryScreen, AudioCaptureMode.CAPTURE)
})
}
Em segundo lugar, gostaríamos de informar a nossa IU se existe uma sessão ativa
de compartilhamento neste momento. É necessário decidir se deve mostrar o
botão Start
ou Stop
. Vamos criar uma propriedade observável e vinculá-la aos
eventos CaptureSessionStarted
e CaptureSessionsStopped
do JxBrowser:
var isSharing by mutableStateOf(false)
private set
init {
// ...
// Atualiza a variável de estado `isSharing` quando uma sessão inicia e pára.
browser.subscribe<CaptureSessionStarted> { event: CaptureSessionStarted ->
isSharing = true
event.capture().subscribe<CaptureSessionStopped> {
isSharing = false
}
}
}
A última coisa a fazer é adicionar mais dois métodos públicos que chamam seus equivalentes em JavaScript:
fun startScreenSharing() = executeJavaScript("startScreenSharing()")
fun stopScreenSharing() = executeJavaScript("stopScreenSharing()")
É isso!
Quando executada localmente, a aplicação tem o seguinte aspecto:
Código fonte
Os exemplos de código são fornecidos sob a licença MIT e estão disponíveis em GitHub. Você pode iniciar o servidor e ambas as aplicações Compose executando os comandos abaixo. Para maior comodidade, você pode fazê-lo em sessões de terminal individuais.
./gradlew :compose:screen-share:server:run
./gradlew :compose:screen-share:sender:run
./gradlew :compose:screen-share:receiver:run
Enviando…
Desculpe, o envio foi interrompido
Tente novamente. Se o problema persistir, contate-nos através do endereço info@teamdev.com.
A sua chave de avaliação pessoal de JxBrowser e o guia de início rápido chegarão à sua caixa de entrada de e-mail dentro de alguns minutos.
Execução em diferentes PCs
Como bônus, você pode facilmente fazer com que este exemplo funcione em diferentes PCs sem expor um servidor de sinalização rodando localmente. O PeerJS oferece uma versão gratuita armazenada na nuvem do PeerServer. A biblioteca se conecta automaticamente ao servidor da nuvem pública se não lhe for atribuída um servidor específico para utilizar. Observe que, ele está operacional no momento em que escrevo, mas o seu tempo de atividade não é garantido.
Para testar, é necessário remover uma passagem explícita do servidor para o construtor Peer nos componentes WebRTC emissores e receptores:
// Utilizar o servidor.
const peer = new Peer(RECEIVER_PEER_ID, signalingServer);
// Utilizar a instância armazenada na nuvem.
const peer = new Peer(RECEIVER_PEER_ID);
Além disso, tenha em atenção que estará compartilhando este servidor público com outras pessoas, e as IDs de pares podem colidir porque são definidas manualmente.
Conclusão
Neste artigo, demonstramos como compartilhar a tela em um aplicativo Compose e exibir o fluxo de vídeo em outro usando JxBrowser e WebRTC. Aproveitando a integração do Chromium no JxBrowser e da biblioteca PeerJS para WebRTC, podemos criar uma aplicação funcional de compartilhamento de tela em pouco tempo.