OpenSakura
MekaHime 첫 AI 동반자, 오픈소스. 26가지 감정·실제 음성·실시간 답변·기억 기능을 PC에서 구동, 곧 디스코드 모드 제공
Discord와 연동해 친구와 그룹을 만들고 화면·카메라를 실시간 공유하거나, 계정·서버 없이 로컬 방을 생성합니다
Entre com o Discord, monte grupos com seus amigos e compartilhe a tela e a câmera em tempo real. Ou crie uma sala local, sem conta e sem servidor.
소셜GitHub @miguelsoeth온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
저장소 정보는 아직 수집하지 않았습니다.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
API do ScreenCord: login com Discord, amigos, grupos em tempo real (SignalR) e relay das transmissões. ASP.NET Core/.NET 10.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Este repositório contém a API central (
src/ScreenCord.Api), seus testes e os contratos compartilhados (src/ScreenCord.Contracts). O aplicativo desktop fica no repositório screencord.app. Os contratos existem nos dois repositórios: ao mudar um DTO, atualize os dois.
Sistema com dois componentes:
Importante: os "amigos" aqui são do ScreenCord, não a sua lista de amigos do Discord. A API oficial do Discord não permite ler a lista de amigos (veja Limitações reais da API do Discord). Para alguém aparecer na busca, essa pessoa precisa ter entrado neste sistema com a conta dela.
client_secret).GET /users/@me: nome de exibição, nome de usuário, ID, avatar e URL do avatar.@fulano), enviar solicitação, aceitar, recusar, cancelar e remover amigo (com confirmação).conectados/integrantes (ex.: 1/2). Quem está conectado aparece listado (foto e nome) logo abaixo do grupo; a seta à esquerda recolhe ou expande essa lista. Clicar no nome conecta você à sala; clicar na contagem abre os integrantes (foto, nome e quem está conectado) e permite adicionar amigos. Qualquer integrante adiciona os próprios amigos ou sai do grupo; só quem criou o grupo vê Excluir grupo, que o apaga para todos (com confirmação). Entradas e saídas aparecem na hora para todos.GET /limits, ajustável no painel /admin sem novo deploy); nas salas locais do app, em que a mídia vai direto entre os computadores, até 1080p a 60 fps e 5000 kbps. A taxa de bits não é escolhida: começa no teto e se adapta à conexão de quem compartilha, sem passar dele. O padrão é a maior qualidade a 30 fps dentro do limite, com áudio; a última escolha é lembrada enquanto o app está aberto. Durante o compartilhamento, a engrenagem ao lado de parar abre a mesma escolha para alterar as configurações (ou a tela/janela): o compartilhamento recomeça por baixo dos panos e quem está assistindo continua vendo, já com a nova configuração. Quem compartilha aparece com o selo AO VIVO na lista de conectados; clicar nele mostra a transmissão na área principal da janela, uma de cada vez (abrir outra troca a atual; o X da barra para de assistir), com volume, mudo e tela cheia (botão, F11 ou duplo clique; Esc, F11 ou duplo clique para sair). Clicar em AO VIVO de outro grupo entra na sala dele antes. Sair da sala, trocar de sala ou perder a conexão encerra o compartilhamento e fecha a transmissão que você assistia.http://127.0.0.1:47821/callback.client_secret).O único escopo solicitado é identify.
O mesmo Application ID vai nos dois componentes, em arquivos appsettings.Local.json (ignorados pelo Git):
src/ScreenCord/appsettings.Local.json (aplicativo):
{
"Discord": { "ClientId": "SEU_APPLICATION_ID" }
}
src/ScreenCord.Api/appsettings.Local.json (API):
{
"Discord": { "ClientId": "SEU_APPLICATION_ID" }
}
A API usa o Client ID para recusar tokens do Discord emitidos para outros aplicativos.
Opções do aplicativo:
| Chave | Padrão | Descrição |
|---|---|---|
Discord:ClientId | (vazio) | Application ID. Obrigatório. |
Discord:RedirectPort | 47821 | Porta do callback local. Se mudar, atualize o redirect no portal. |
Discord:Scopes | identify | Escopos separados por espaço. |
Discord:LoginTimeoutSeconds | 300 | Espera máxima pela autorização no navegador. |
Api:BaseUrl | http://localhost:5080 | Endereço da API central. Precisa ser https, exceto para localhost. |
Opções da API:
| Chave | Padrão | Descrição |
|---|---|---|
Discord:ClientId | (vazio) | Application ID. Obrigatório. |
ConnectionStrings:Default | Data Source=screencord.db | Banco SQLite. |
Session:LifetimeDays | 30 | Validade da sessão da API. |
# no clone do screencord.app, ao lado deste
powershell -ExecutionPolicy Bypass -File ..\screencord.app\screencord.ps1
Menu navegável com as setas (↑/↓, Enter, Esc) para subir, parar e recompilar a API e o app, acompanhar os logs ao vivo e rodar os testes. Os processos rodam em segundo plano e continuam vivos ao fechar o menu; PIDs e logs ficam em .screencord/.
Também aceita ações diretas: status, start-api, stop-api, start-app, stop-app, start-all, stop-all, build, rebuild, test, logs-api, logs-app. Exemplo: .\screencord.ps1 start-all.
Em um terminal, a API (fica em http://localhost:5080; o banco é criado na primeira execução):
dotnet run --project src/ScreenCord.Api
Em outro, o aplicativo:
dotnet run --project src/ScreenCord
Sem a API no ar, o perfil continua funcionando e a seção "Amigos" mostra um erro com o botão "Tentar novamente".
Para testar os grupos em tempo real, abra duas instâncias do aplicativo com contas diferentes que sejam amigas, crie um grupo em uma, adicione a outra pela contagem de integrantes e clique no nome do grupo nas duas. Para testar o compartilhamento de tela, clique no botão de compartilhar em uma delas e no selo AO VIVO na outra.
Para testar a amizade entre duas contas na mesma máquina, entre com a primeira, clique em Desconectar, entre com a segunda, busque a primeira pelo nome de usuário e envie a solicitação.
dotnet test ScreenCord.Api.sln
Nenhum teste acessa o Discord: as respostas são simuladas. Os testes da API sobem o servidor real em memória, com SQLite em memória. Os testes de mídia codificam e decodificam vídeo de verdade com o Media Foundation do Windows (usando o encoder de hardware, se houver).
Aplicativo para Windows x64 (executável único, sem exigir .NET instalado):
dotnet publish src/ScreenCord -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o publish/app
Distribua ScreenCord.exe com appsettings.json contendo o Discord:ClientId e o Api:BaseUrl do servidor.
API: a produção roda em https://api.screencord.miguelsoeth.online (Docker + Caddy no VPS) e o deploy é automático ao criar uma tag de versão:
git tag v0.2.0
git push origin v0.2.0
O processo completo (versionamento, workflows, VPS, rollback e backups) está em CLAUDE.md.
Observações de produção:
http que não seja localhost). O token do Discord trafega até a API no login./hubs/groups e /media/...) e não deve guardar as respostas em buffer, senão o vídeo atrasa. Conte com a banda de saída do compartilhamento de tela (taxa × espectadores).EnsureCreated. Para evoluir o esquema sem perder dados, adote migrações do EF Core. Para mais de uma instância, troque o SQLite por um banco servidor.Com a API rodando em Development (o padrão do screencord.ps1 e do dotnet run), a documentação interativa fica em:
Fora de Development as duas rotas não são publicadas.
Todas as rotas, exceto /health, /limits, /auth/discord, /telemetry/errors e o painel /admin, exigem Authorization: Bearer <token de sessão>.
| Método e rota | Descrição |
|---|---|
GET /health | Verificação de disponibilidade. |
GET /limits | Limites do compartilhamento pela API: { relay: { maxHeight, maxFrameRate, maxBitrateKbps, profiles: [{ height, frameRate }] } }. profiles são as combinações aceitas; uma combinação maior cobre as menores (720p a 60 fps cobre 540p a 60 fps e 720p a 30 fps). maxHeight e maxFrameRate são o maior de cada e valem sozinhos para apps antigos, que não leem profiles. Padrão: 720p a 60 fps ou 1080p a 30 fps, até 3000 kbps. Muda pelo painel /admin (vale na hora, fica no banco) ou, como padrão inicial, pela seção RelayLimits. |
POST /auth/discord | Corpo { "accessToken": "<token do Discord>" }. Confirma o token com o Discord, cria/atualiza o usuário e devolve { token, expiresAtUtc, user }. |
POST /auth/logout | Encerra a sessão atual. |
GET /me | Usuário da sessão. |
GET /users/search?q= | Busca por nome de usuário ou de exibição (mínimo 2 caracteres, até 20 resultados, sem o próprio usuário). Cada item traz relationship. |
GET /friends | Amigos confirmados. |
DELETE /friends/{userId} | Desfaz a amizade. |
GET /friends/requests | Solicitações incoming e outgoing. |
POST /friends/requests | Corpo { "userId": "<guid>" }. Envia a solicitação; se a outra pessoa já tinha pedido, confirma a amizade. |
POST /friends/requests/{userId}/accept | Aceita a solicitação recebida de userId. |
DELETE /friends/requests/{userId} | Recusa uma solicitação recebida ou cancela uma enviada. |
GET /groups | Seus grupos, com memberCount, connectedUsers (id, nome e avatar de quem está na sala) e presenceVersion. |
POST /groups | Corpo { "name": "..." } (1 a 40 caracteres). Cria o grupo com você como integrante. Cada conta pode ter até 5 grupos criados por ela (contam os que ela ainda integra); acima disso, 409 . |
relationship é None, Friend, PendingOutgoing ou PendingIncoming. Erros retornam { "code": "...", "message": "..." }.
Tudo dentro da própria API, sem serviço externo:
X-Trace-Id. O aplicativo manda traceparent (W3C) em cada chamada, então o trace é o mesmo dos dois lados.ErrorEvents do SQLite, com o trace, a rota, a versão do app e o usuário.POST /telemetry/errors (ClientErrorReport nos Contracts) recebe os avisos e erros do app em lotes, sem login (20 relatórios/min por IP, até 50 entradas cada, textos cortados, fora da versão mínima). Identifica só a instalação (id aleatório) e a execução.GET /admin, protegido pelo token Observability:DashboardToken (cabeçalho X-Admin-Token; a página pede e guarda no navegador). Sem token configurado, /admin responde 404. Mostra os indicadores, os gráficos por minuto, as rotas, os problemas agrupados (mesma exceção e pilha, ou mesmo texto sem números) e cada ocorrência com a pilha, o estado do app no erro, as linhas do log de antes e o computador (placas de vídeo, driver, telas), quando o app os manda (0.7.0 em diante); clicar no trace junta o erro do app e o da API daquela requisição. Em Development o token é dev: http://localhost:5080/admin.UserActivities (usuário e dia, nada mais). O painel mostra os ativos hoje, em 7 e em 30 dias e um gráfico diário dos últimos 30 dias. Quem usa o app sem conta (só salas locais) não fala com a API e não entra na conta. Os dias ficam 90 dias.Settings do SQLite e sobrevive a deploys; "Restaurar padrão" volta ao padrão do código ou da seção RelayLimits (RelayLimits__Profiles__0__Height=720, RelayLimits__Profiles__0__FrameRate=60, ..., RelayLimits__MaxBitrateKbps; / ainda valem, como um par só, quando não há ). Por baixo: , e , com o mesmo token.O hub /hubs/groups mantém uma conexão persistente (WebSocket, com fallback automático do SignalR), autenticada com o mesmo token de sessão no cabeçalho Authorization — o token nunca vai na URL.
| Mensagem | Direção | Descrição |
|---|---|---|
JoinGroup(groupId) | app → API | Entra na sala (saindo da anterior). Retorna a presença atual, ou null se você não for integrante. |
LeaveGroup() | app → API | Sai da sala atual. Fechar a conexão tem o mesmo efeito. |
PresenceChanged(presence) | API → app | { groupId, connectedUsers, version }, enviado a todos os integrantes a cada entrada ou saída. |
GroupChanged(group) | API → app | Grupo criado ou com integrantes alterados (inclusive para quem acabou de ser adicionado). |
GroupRemoved(groupId) | API → app | Você saiu do grupo (avisa as suas outras janelas). |
StartScreenShare(start) | app → API | { width, height, frameRate, bitrateKbps, hasAudio, kind } (kind: Screen, o padrão, ou Camera). Começa a transmitir na sala atual (cada pessoa tem no máximo uma transmissão de cada kind; tela e câmera podem estar no ar juntas, e pedir um kind que já está no ar o substitui); retorna { userId, width, height, frameRate, bitrateKbps, hasAudio, kind }, ou null se a conexão não estiver numa sala, o grupo não aceitar esse kind ou a configuração passar dos limites de GET /limits (padrão: 720p a 60 fps ou 1080p a 30 fps, até 3000 kbps). |
StopScreenShare() | app → API | Encerra todas as transmissões desta conexão (tela e câmera). Sair da sala, trocar de sala ou desconectar também encerra. |
StopShare(kind) | app → API | Encerra só a transmissão desse kind desta conexão; a outra continua. |
A presença (PresenceChanged e os grupos de GET /groups) traz também screenShares: quem está compartilhando na sala e com que configuração.
version cresce a cada mudança de presença (inclusive início e fim de compartilhamento); o app descarta notificações mais antigas que a última aplicada, então mensagens fora de ordem não deixam a lista errada. Ao reconectar, o app recarrega os grupos e volta para a sala em que estava. A presença fica em memória numa instância da API; para rodar várias instâncias seria preciso um backplane (ex.: Redis).
quem compartilha API quem assiste
Windows Graphics Capture ─┐
(tela ou janela) │ GPU: BGRA → NV12, escala WebSocket /media/{grupo}/{id} WebSocket /media/{grupo}/{id}
H.264 (Media Foundation) ─┼──────────────────────────► repasse sem decodificar ───────► H.264 → BGRA → janela
Opus (áudio por processo)─┘ (uma fila por espectador) Opus → WASAPI
/media/{groupId}/{sharerId} (?kind=camera para a câmera; sem o parâmetro vale a tela, ou a câmera quando é a única da pessoa, o formato das versões do app que só tinham uma transmissão por pessoa), autenticado pelo mesmo cabeçalho Authorization (o token nunca vai na URL). Quem compartilha conecta no próprio id depois de StartScreenShare (antes disso recebe 409); quem assiste conecta no id de quem compartilha e precisa estar na sala (403 se não estiver). Cada mensagem binária é um pacote: 1 byte de tipo (1 vídeo, 2 áudio, 3 pedido de quadro-chave, 4 pausar o vídeo, 5 retomar o vídeo), 1 byte de flags (bit 0 = quadro-chave), 8 bytes de carimbo de tempo em µs (little-endian) e o conteúdo. Vídeo vai em H.264 Annex B com SPS/PPS antes de cada quadro-chave; pacotes acima de 4 MB são recusados.MediaQueue, nos Contracts): se o pacote mais antigo espera mais de 400 ms, o espectador perde quadros até o próximo quadro-chave, pedido assim que a fila esvazia, em vez de ver a imagem em câmera lenta, cada vez mais atrasada. Não afeta os outros espectadores. O envio de um quadro-chave, que é grande, não conta como atraso. Do lado de quem compartilha (e nas salas locais) vale o mesmo para a fila de envio. Quem assiste também mede o próprio atraso pelos carimbos de tempo: passando de 1 s (ex.: o computador não dá conta de decodificar), descarta até o próximo quadro-chave. Quem assiste e fica sem imagem pede outro quadro-chave sozinho.Limitações: a mídia passa pela API, então o tráfego de saída do servidor é a taxa escolhida vezes o número de espectadores (ex.: 3000 kbps para 4 pessoas = 12 Mbps). Para muitos espectadores, o caminho seria WebRTC com um SFU. Conteúdo protegido (ex.: vídeos com DRM em alguns navegadores) aparece preto, como em qualquer captura do Windows. Janelas minimizadas não geram imagem até serem restauradas.
src/ScreenCord Aplicativo WPF
├── Models/ DTOs do Discord e modelos de exibição
├── Services/ OAuth2, API do Discord, perfil, cliente da API central (amigos, grupos), conexão SignalR e compartilhamento de tela
├── Media/ Captura de tela, H.264 (Media Foundation), áudio Opus/WASAPI e canal de mídia
├── Security/ PKCE, sessão do Discord e armazenamento DPAPI
├── Infrastructure/ Opções, DI, erros centralizados, rate limit, navegador
├── ViewModels/ Estado e comandos (MVVM)
├── Views/ Themes/ Janela e tema escuro
src/ScreenCord.Api API central
├── Auth/ Verificação do token do Discord e autenticação por sessão
├── Data/ EF Core: Users, Friendships, Sessions, Groups, GroupMembers
├── Endpoints/ Rotas de login, busca, amizades e grupos; canal de mídia (WebSocket)
└── Realtime/ Hub SignalR de grupos, rastreador de presença e repasse da mídia
src/ScreenCord.Contracts DTOs compartilhados entre app e API
tests/ScreenCord.Tests, tests/ScreenCord.Api.Tests
Como o login chega à API:
POST /auth/discord.GET /oauth2/@me no Discord, que informa o usuário e para qual aplicativo o token foi emitido. Tokens de outro aplicativo, sem o escopo identify ou inválidos são recusados.client_secret em lugar nenhum.state aleatório de 256 bits, comparado em tempo constante.127.0.0.1, só GET no caminho configurado, página estática, encerrado ao fim do login.discord.com e, no login, para a API central configurada (HTTPS obrigatório fora de localhost).Qualquer usuário autenticado pode encontrar qualquer outro usuário do sistema pela busca (nome de usuário, nome de exibição, ID do Discord e avatar). Informe isso a quem for usar o sistema. A API guarda somente esses dados de perfil, as datas de criação/último login e as relações de amizade, além dos relatórios de erro do aplicativo (versão, Windows, id aleatório da instalação, mensagens de log e pilhas de exceção), apagados depois de 30 dias.
Verificado na documentação oficial (https://docs.discord.com/developers) em outubro de 2026:
relationships.read pertence ao Discord Social SDK (SDK nativo voltado a jogos) e não é utilizável aqui. Por isso este sistema mantém a própria relação de amigos.invalid_client.client_secret depende do mesmo modo. Se o Discord recusar, a sessão local é apagada e o app volta para "Conectar com Discord".A API do ScreenCord é software livre sob a GNU Affero General Public License v3.0. Quem rodar uma versão modificada como serviço para outras pessoas precisa oferecer a elas o código-fonte dessa versão, também sob a AGPL-3.0. As licenças dos componentes de terceiros estão em THIRD-PARTY-NOTICES.txt.
O nome ScreenCord, o logo e os mascotes não fazem parte da licença: um fork precisa de outro nome e outra identidade visual.
Para relatar uma vulnerabilidade, veja SECURITY.md.
group_limitGET /groups/{groupId} | Integrantes, cada um com isConnected. Só para integrantes. |
POST /groups/{groupId}/members | Corpo { "userId": "<guid>" }. Adiciona um amigo seu (limite de 10 integrantes). |
DELETE /groups/{groupId}/members/me | Sai do grupo; o grupo é apagado quando o último integrante sai. |
DELETE /groups/{groupId} | Exclui o grupo para todos (só quem criou; 403 not_group_owner para os outros). Todos saem da sala, os compartilhamentos terminam e cada integrante recebe GroupRemoved. GET /groups/{groupId} traz canDelete. |
PUT /groups/{groupId}/settings | Corpo { "allowScreenShare": true, "allowCamera": true }. Só quem criou (403 not_group_owner). As transmissões do tipo desligado terminam e todos recebem GroupChanged. GET /groups e GET /groups/{groupId} trazem settings (e canEdit nos detalhes). Grupos criados antes da opção aceitam os dois (colunas nulas). |
MaxHeightMaxFrameRateProfilesGETPUTDELETE /admin/api/limitsObservability:RetentionDays dias (padrão 30), no máximo Observability:MaxEvents (padrão 50000).StartScreenShare. Por canal de quem compartilha, aceita até o dobro dela mais 512 kbps, com rajada de 3 s (no mínimo 1 MB, para caber um quadro-chave); o excesso é descartado (quem assiste recomeça do próximo quadro-chave) e, se o descartado numa janela de 10 s passar de 3 s da taxa permitida, o canal é fechado com PolicyViolation e o compartilhamento termina (PublisherRateLimit). O app, com taxa constante e áudio a 128 kbps, fica bem abaixo disso.