Guia de início rápido: adicione acesso a mídia bruta ao seu aplicativo
Neste guia de início rápido, você aprenderá a implementar o acesso bruto à mídia usando o SDK de Chamada dos Serviços de Comunicação do Azure para Unity. O SDK de Chamada dos Serviços de Comunicação do Azure oferece APIs que permitem que os aplicativos gerem seus próprios quadros de vídeo para enviar ou renderizar quadros de vídeo brutos de participantes remotos em uma chamada. Este guia de início rápido se baseia em Guia de início rápido: adicione chamadas de vídeo 1:1 ao seu aplicativo para Unity.
Acesso ao RawVideo
Como o aplicativo gera os quadros de vídeo, o aplicativo deve informar o SDK de Chamada dos Serviços de Comunicação do Azure sobre os formatos de vídeo que o aplicativo pode gerar. Essas informações permitem que o SDK de Chamada dos Serviços de Comunicação do Azure escolha a melhor configuração de formato de vídeo para as condições de rede naquele momento.
Vídeo Virtual
Resoluções de vídeo suportadas
Relação de aspeto | Resolução | FPS máximo |
---|---|---|
16x9 | 1080p | 30 |
16x9 | 720p | 30 |
16x9 | 540p | 30 |
16x9 | 480p | 30 |
16x9 | 360p | 30 |
16x9 | 270p | 15 |
16x9 | 240p | 15 |
16x9 | 180p | 15 |
4x3 | VGA (640x480) | 30 |
4x3 | 424x320 | 15 |
4x3 | QVGA (320x240) | 15 |
4x3 | 212x160 | 15 |
Siga as etapas aqui Guia de início rápido: adicione chamadas de vídeo 1:1 ao seu aplicativo para criar o jogo Unity. O objetivo é obter um
CallAgent
objeto pronto para iniciar a chamada. Encontre o código finalizado para este início rápido no GitHub.Crie uma matriz usando
VideoFormat
o VideoStreamPixelFormat suportado pelo SDK. Quando vários formatos estão disponíveis, a ordem dos formatos na lista não influencia ou prioriza qual deles é usado. Os critérios para a seleção do formato são baseados em fatores externos, como a largura de banda da rede.var videoStreamFormat = new VideoStreamFormat { Resolution = VideoStreamResolution.P360, // For VirtualOutgoingVideoStream the width/height should be set using VideoStreamResolution enum PixelFormat = VideoStreamPixelFormat.Rgba, FramesPerSecond = 15, Stride1 = 640 * 4 // It is times 4 because RGBA is a 32-bit format }; VideoStreamFormat[] videoStreamFormats = { videoStreamFormat };
Crie
RawOutgoingVideoStreamOptions
e definaFormats
com o objeto criado anteriormente.var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions { Formats = videoStreamFormats };
Crie uma instância de
VirtualOutgoingVideoStream
usando aRawOutgoingVideoStreamOptions
instância que você criou anteriormente.var rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
Inscreva-se no
RawOutgoingVideoStream.FormatChanged
delegado. Este evento informa sempre que o foi alterado aVideoStreamFormat
partir de um dos formatos de vídeo fornecidos na lista.rawOutgoingVideoStream.FormatChanged += (object sender, VideoStreamFormatChangedEventArgs args) { VideoStreamFormat videoStreamFormat = args.Format; }
Inscreva-se no
RawOutgoingVideoStream.StateChanged
delegado. Este evento informa sempre que oState
mudou.rawOutgoingVideoStream.StateChanged += (object sender, VideoStreamFormatChangedEventArgs args) { CallVideoStream callVideoStream = e.Stream; switch (callVideoStream.Direction) { case StreamDirection.Outgoing: OnRawOutgoingVideoStreamStateChanged(callVideoStream as OutgoingVideoStream); break; case StreamDirection.Incoming: OnRawIncomingVideoStreamStateChanged(callVideoStream as IncomingVideoStream); break; } }
Manipule transações brutas de estado de fluxo de vídeo de saída, como Start e Stop, e comece a gerar quadros de vídeo personalizados ou suspenda o algoritmo de geração de quadros.
private async void OnRawOutgoingVideoStreamStateChanged(OutgoingVideoStream outgoingVideoStream) { switch (outgoingVideoStream.State) { case VideoStreamState.Started: switch (outgoingVideoStream.Kind) { case VideoStreamKind.VirtualOutgoing: outgoingVideoPlayer.StartGenerateFrames(outgoingVideoStream); // This is where a background worker thread can be started to feed the outgoing video frames. break; } break; case VideoStreamState.Stopped: switch (outgoingVideoStream.Kind) { case VideoStreamKind.VirtualOutgoing: break; } break; } }
Aqui está um exemplo do gerador de quadros de vídeo de saída:
private unsafe RawVideoFrame GenerateRawVideoFrame(RawOutgoingVideoStream rawOutgoingVideoStream) { var format = rawOutgoingVideoStream.Format; int w = format.Width; int h = format.Height; int rgbaCapacity = w * h * 4; var rgbaBuffer = new NativeBuffer(rgbaCapacity); rgbaBuffer.GetData(out IntPtr rgbaArrayBuffer, out rgbaCapacity); byte r = (byte)random.Next(1, 255); byte g = (byte)random.Next(1, 255); byte b = (byte)random.Next(1, 255); for (int y = 0; y < h; y++) { for (int x = 0; x < w*4; x += 4) { ((byte*)rgbaArrayBuffer)[(w * 4 * y) + x + 0] = (byte)(y % r); ((byte*)rgbaArrayBuffer)[(w * 4 * y) + x + 1] = (byte)(y % g); ((byte*)rgbaArrayBuffer)[(w * 4 * y) + x + 2] = (byte)(y % b); ((byte*)rgbaArrayBuffer)[(w * 4 * y) + x + 3] = 255; } } // Call ACS Unity SDK API to deliver the frame rawOutgoingVideoStream.SendRawVideoFrameAsync(new RawVideoFrameBuffer() { Buffers = new NativeBuffer[] { rgbaBuffer }, StreamFormat = rawOutgoingVideoStream.Format, TimestampInTicks = rawOutgoingVideoStream.TimestampInTicks }).Wait(); return new RawVideoFrameBuffer() { Buffers = new NativeBuffer[] { rgbaBuffer }, StreamFormat = rawOutgoingVideoStream.Format }; }
Nota
unsafe
modificador é usado neste método, uma vez queNativeBuffer
requer acesso a recursos de memória nativa. Portanto,Allow unsafe
a opção precisa ser habilitada no Unity Editor também.Da mesma forma, podemos lidar com quadros de vídeo de entrada em resposta ao evento de fluxo
StateChanged
de vídeo.private void OnRawIncomingVideoStreamStateChanged(IncomingVideoStream incomingVideoStream) { switch (incomingVideoStream.State) { case VideoStreamState.Available: { var rawIncomingVideoStream = incomingVideoStream as RawIncomingVideoStream; rawIncomingVideoStream.RawVideoFrameReceived += OnRawVideoFrameReceived; rawIncomingVideoStream.Start(); break; } case VideoStreamState.Stopped: break; case VideoStreamState.NotAvailable: break; } } private void OnRawVideoFrameReceived(object sender, RawVideoFrameReceivedEventArgs e) { incomingVideoPlayer.RenderRawVideoFrame(e.Frame); } public void RenderRawVideoFrame(RawVideoFrame rawVideoFrame) { var videoFrameBuffer = rawVideoFrame as RawVideoFrameBuffer; pendingIncomingFrames.Enqueue(new PendingFrame() { frame = rawVideoFrame, kind = RawVideoFrameKind.Buffer }); }
É altamente recomendável gerenciar quadros de vídeo de entrada e saída através de um mecanismo de buffer para evitar sobrecarregar o método de retorno de
MonoBehaviour.Update()
chamada, que deve ser mantido leve e evitar tarefas pesadas da CPU ou da rede e garantir uma experiência de vídeo mais suave. Essa otimização opcional é deixada para os desenvolvedores decidirem o que funciona melhor em seus cenários.Aqui está um exemplo de como os quadros de entrada podem ser renderizados para um Unity
VideoTexture
chamandoGraphics.Blit
para fora de uma fila interna:private void Update() { if (pendingIncomingFrames.TryDequeue(out PendingFrame pendingFrame)) { switch (pendingFrame.kind) { case RawVideoFrameKind.Buffer: var videoFrameBuffer = pendingFrame.frame as RawVideoFrameBuffer; VideoStreamFormat videoFormat = videoFrameBuffer.StreamFormat; int width = videoFormat.Width; int height = videoFormat.Height; var texture = new Texture2D(width, height, TextureFormat.RGBA32, mipChain: false); var buffers = videoFrameBuffer.Buffers; NativeBuffer buffer = buffers.Count > 0 ? buffers[0] : null; buffer.GetData(out IntPtr bytes, out int signedSize); texture.LoadRawTextureData(bytes, signedSize); texture.Apply(); Graphics.Blit(source: texture, dest: rawIncomingVideoRenderTexture); break; case RawVideoFrameKind.Texture: break; } pendingFrame.frame.Dispose(); } }
Neste guia de início rápido, você aprenderá a implementar o acesso bruto a mídia usando o SDK de Chamada dos Serviços de Comunicação do Azure para Windows. O SDK de Chamada dos Serviços de Comunicação do Azure oferece APIs que permitem que os aplicativos gerem seus próprios quadros de vídeo para enviar aos participantes remotos de uma chamada. Este guia de início rápido se baseia em Guia de início rápido: adicione chamadas de vídeo 1:1 ao seu aplicativo para Windows.
Acesso RawAudio
O acesso a mídia de áudio bruto dá acesso ao fluxo de áudio da chamada recebida, juntamente com a capacidade de visualizar e enviar fluxos de áudio de saída personalizados durante uma chamada.
Enviar áudio de saída Raw
Crie um objeto options especificando as propriedades de fluxo bruto que queremos enviar.
RawOutgoingAudioStreamProperties outgoingAudioProperties = new RawOutgoingAudioStreamProperties()
{
Format = ACSAudioStreamFormat.Pcm16Bit,
SampleRate = AudioStreamSampleRate.Hz48000,
ChannelMode = AudioStreamChannelMode.Stereo,
BufferDuration = AudioStreamBufferDuration.InMs20
};
RawOutgoingAudioStreamOptions outgoingAudioStreamOptions = new RawOutgoingAudioStreamOptions()
{
Properties = outgoingAudioProperties
};
Crie um RawOutgoingAudioStream
e anexe-o para participar das opções de chamada e o fluxo é iniciado automaticamente quando a chamada é conectada.
JoinCallOptions options = JoinCallOptions(); // or StartCallOptions()
OutgoingAudioOptions outgoingAudioOptions = new OutgoingAudioOptions();
RawOutgoingAudioStream rawOutgoingAudioStream = new RawOutgoingAudioStream(outgoingAudioStreamOptions);
outgoingAudioOptions.Stream = rawOutgoingAudioStream;
options.OutgoingAudioOptions = outgoingAudioOptions;
// Start or Join call with those call options.
Anexar fluxo a uma chamada
Ou você também pode anexar o fluxo a uma instância existente Call
:
await call.StartAudio(rawOutgoingAudioStream);
Comece a enviar amostras brutas
Só podemos começar a enviar dados quando o estado do fluxo for AudioStreamState.Started
.
Para observar a alteração do estado do fluxo de áudio, adicione um ouvinte ao OnStateChangedListener
evento.
unsafe private void AudioStateChanged(object sender, AudioStreamStateChanged args)
{
if (args.AudioStreamState == AudioStreamState.Started)
{
// We can now start sending samples.
}
}
outgoingAudioStream.StateChanged += AudioStateChanged;
Quando o fluxo começou, podemos começar a enviar MemoryBuffer
amostras de áudio para a chamada.
O formato do buffer de áudio deve corresponder às propriedades de fluxo especificadas.
void Start()
{
RawOutgoingAudioStreamProperties properties = outgoingAudioStream.Properties;
RawAudioBuffer buffer;
new Thread(() =>
{
DateTime nextDeliverTime = DateTime.Now;
while (true)
{
MemoryBuffer memoryBuffer = new MemoryBuffer((uint)outgoingAudioStream.ExpectedBufferSizeInBytes);
using (IMemoryBufferReference reference = memoryBuffer.CreateReference())
{
byte* dataInBytes;
uint capacityInBytes;
((IMemoryBufferByteAccess)reference).GetBuffer(out dataInBytes, out capacityInBytes);
// Use AudioGraph here to grab data from microphone if you want microphone data
}
nextDeliverTime = nextDeliverTime.AddMilliseconds(20);
buffer = new RawAudioBuffer(memoryBuffer);
outgoingAudioStream.SendOutgoingAudioBuffer(buffer);
TimeSpan wait = nextDeliverTime - DateTime.Now;
if (wait > TimeSpan.Zero)
{
Thread.Sleep(wait);
}
}
}).Start();
}
Receber áudio Raw Incoming
Também podemos receber amostras de fluxo de áudio de chamada como MemoryBuffer
se quiséssemos processar o fluxo de áudio de chamada antes da reprodução.
Crie um RawIncomingAudioStreamOptions
objeto especificando as propriedades de fluxo bruto que queremos receber.
RawIncomingAudioStreamProperties properties = new RawIncomingAudioStreamProperties()
{
Format = AudioStreamFormat.Pcm16Bit,
SampleRate = AudioStreamSampleRate.Hz44100,
ChannelMode = AudioStreamChannelMode.Stereo
};
RawIncomingAudioStreamOptions options = new RawIncomingAudioStreamOptions()
{
Properties = properties
};
Criar um RawIncomingAudioStream
e anexá-lo para participar de opções de chamada
JoinCallOptions options = JoinCallOptions(); // or StartCallOptions()
RawIncomingAudioStream rawIncomingAudioStream = new RawIncomingAudioStream(audioStreamOptions);
IncomingAudioOptions incomingAudioOptions = new IncomingAudioOptions()
{
Stream = rawIncomingAudioStream
};
options.IncomingAudioOptions = incomingAudioOptions;
Ou também podemos anexar o fluxo a uma instância existente Call
:
await call.startAudio(context, rawIncomingAudioStream);
Para começar a receber buffers de áudio brutos do fluxo de entrada, adicione ouvintes ao estado do fluxo de entrada e aos eventos recebidos do buffer.
unsafe private void OnAudioStateChanged(object sender, AudioStreamStateChanged args)
{
if (args.AudioStreamState == AudioStreamState.Started)
{
// When value is `AudioStreamState.STARTED` we'll be able to receive samples.
}
}
private void OnRawIncomingMixedAudioBufferAvailable(object sender, IncomingMixedAudioEventArgs args)
{
// Received a raw audio buffers(MemoryBuffer).
using (IMemoryBufferReference reference = args.IncomingAudioBuffer.Buffer.CreateReference())
{
byte* dataInBytes;
uint capacityInBytes;
((IMemoryBufferByteAccess)reference).GetBuffer(out dataInBytes, out capacityInBytes);
// Process the data using AudioGraph class
}
}
rawIncomingAudioStream.StateChanged += OnAudioStateChanged;
rawIncomingAudioStream.MixedAudioBufferReceived += OnRawIncomingMixedAudioBufferAvailable;
Acesso ao RawVideo
Como o aplicativo gera os quadros de vídeo, o aplicativo deve informar o SDK de Chamada dos Serviços de Comunicação do Azure sobre os formatos de vídeo que o aplicativo pode gerar. Essas informações permitem que o SDK de Chamada dos Serviços de Comunicação do Azure escolha a melhor configuração de formato de vídeo para as condições de rede naquele momento.
Vídeo Virtual
Resoluções de vídeo suportadas
Relação de aspeto | Resolução | FPS máximo |
---|---|---|
16x9 | 1080p | 30 |
16x9 | 720p | 30 |
16x9 | 540p | 30 |
16x9 | 480p | 30 |
16x9 | 360p | 30 |
16x9 | 270p | 15 |
16x9 | 240p | 15 |
16x9 | 180p | 15 |
4x3 | VGA (640x480) | 30 |
4x3 | 424x320 | 15 |
4x3 | QVGA (320x240) | 15 |
4x3 | 212x160 | 15 |
Crie uma matriz usando
VideoFormat
o VideoStreamPixelFormat suportado pelo SDK. Quando vários formatos estão disponíveis, a ordem dos formatos na lista não influencia ou prioriza qual deles é usado. Os critérios para a seleção do formato são baseados em fatores externos, como a largura de banda da rede.var videoStreamFormat = new VideoStreamFormat { Resolution = VideoStreamResolution.P720, // For VirtualOutgoingVideoStream the width/height should be set using VideoStreamResolution enum PixelFormat = VideoStreamPixelFormat.Rgba, FramesPerSecond = 30, Stride1 = 1280 * 4 // It is times 4 because RGBA is a 32-bit format }; VideoStreamFormat[] videoStreamFormats = { videoStreamFormat };
Crie
RawOutgoingVideoStreamOptions
e definaFormats
com o objeto criado anteriormente.var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions { Formats = videoStreamFormats };
Crie uma instância de
VirtualOutgoingVideoStream
usando aRawOutgoingVideoStreamOptions
instância que você criou anteriormente.var rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
Inscreva-se no
RawOutgoingVideoStream.FormatChanged
delegado. Este evento informa sempre que o foi alterado aVideoStreamFormat
partir de um dos formatos de vídeo fornecidos na lista.rawOutgoingVideoStream.FormatChanged += (object sender, VideoStreamFormatChangedEventArgs args) { VideoStreamFormat videoStreamFormat = args.Format; }
Crie uma instância da seguinte classe auxiliar para acessar os dados do buffer
[ComImport] [Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] unsafe interface IMemoryBufferByteAccess { void GetBuffer(out byte* buffer, out uint capacity); } [ComImport] [Guid("905A0FEF-BC53-11DF-8C49-001E4FC686DA")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] unsafe interface IBufferByteAccess { void Buffer(out byte* buffer); } internal static class BufferExtensions { // For accessing MemoryBuffer public static unsafe byte* GetArrayBuffer(IMemoryBuffer memoryBuffer) { IMemoryBufferReference memoryBufferReference = memoryBuffer.CreateReference(); var memoryBufferByteAccess = memoryBufferReference as IMemoryBufferByteAccess; memoryBufferByteAccess.GetBuffer(out byte* arrayBuffer, out uint arrayBufferCapacity); GC.AddMemoryPressure(arrayBufferCapacity); return arrayBuffer; } // For accessing MediaStreamSample public static unsafe byte* GetArrayBuffer(IBuffer buffer) { var bufferByteAccess = buffer as IBufferByteAccess; bufferByteAccess.Buffer(out byte* arrayBuffer); uint arrayBufferCapacity = buffer.Capacity; GC.AddMemoryPressure(arrayBufferCapacity); return arrayBuffer; } }
Crie uma instância da seguinte classe auxiliar para gerar 's aleatórios
RawVideoFrame
usandoVideoStreamPixelFormat.Rgba
public class VideoFrameSender { private RawOutgoingVideoStream rawOutgoingVideoStream; private RawVideoFrameKind rawVideoFrameKind; private Thread frameIteratorThread; private Random random = new Random(); private volatile bool stopFrameIterator = false; public VideoFrameSender(RawVideoFrameKind rawVideoFrameKind, RawOutgoingVideoStream rawOutgoingVideoStream) { this.rawVideoFrameKind = rawVideoFrameKind; this.rawOutgoingVideoStream = rawOutgoingVideoStream; } public async void VideoFrameIterator() { while (!stopFrameIterator) { if (rawOutgoingVideoStream != null && rawOutgoingVideoStream.Format != null && rawOutgoingVideoStream.State == VideoStreamState.Started) { await SendRandomVideoFrameRGBA(); } } } private async Task SendRandomVideoFrameRGBA() { uint rgbaCapacity = (uint)(rawOutgoingVideoStream.Format.Width * rawOutgoingVideoStream.Format.Height * 4); RawVideoFrame videoFrame = null; switch (rawVideoFrameKind) { case RawVideoFrameKind.Buffer: videoFrame = GenerateRandomVideoFrameBuffer(rawOutgoingVideoStream.Format, rgbaCapacity); break; case RawVideoFrameKind.Texture: videoFrame = GenerateRandomVideoFrameTexture(rawOutgoingVideoStream.Format, rgbaCapacity); break; } try { using (videoFrame) { await rawOutgoingVideoStream.SendRawVideoFrameAsync(videoFrame); } } catch (Exception ex) { string msg = ex.Message; } try { int delayBetweenFrames = (int)(1000.0 / rawOutgoingVideoStream.Format.FramesPerSecond); await Task.Delay(delayBetweenFrames); } catch (Exception ex) { string msg = ex.Message; } } private unsafe RawVideoFrame GenerateRandomVideoFrameBuffer(VideoStreamFormat videoFormat, uint rgbaCapacity) { var rgbaBuffer = new MemoryBuffer(rgbaCapacity); byte* rgbaArrayBuffer = BufferExtensions.GetArrayBuffer(rgbaBuffer); GenerateRandomVideoFrame(&rgbaArrayBuffer); return new RawVideoFrameBuffer() { Buffers = new MemoryBuffer[] { rgbaBuffer }, StreamFormat = videoFormat }; } private unsafe RawVideoFrame GenerateRandomVideoFrameTexture(VideoStreamFormat videoFormat, uint rgbaCapacity) { var timeSpan = new TimeSpan(rawOutgoingVideoStream.TimestampInTicks); var rgbaBuffer = new Buffer(rgbaCapacity) { Length = rgbaCapacity }; byte* rgbaArrayBuffer = BufferExtensions.GetArrayBuffer(rgbaBuffer); GenerateRandomVideoFrame(&rgbaArrayBuffer); var mediaStreamSample = MediaStreamSample.CreateFromBuffer(rgbaBuffer, timeSpan); return new RawVideoFrameTexture() { Texture = mediaStreamSample, StreamFormat = videoFormat }; } private unsafe void GenerateRandomVideoFrame(byte** rgbaArrayBuffer) { int w = rawOutgoingVideoStream.Format.Width; int h = rawOutgoingVideoStream.Format.Height; byte r = (byte)random.Next(1, 255); byte g = (byte)random.Next(1, 255); byte b = (byte)random.Next(1, 255); int rgbaStride = w * 4; for (int y = 0; y < h; y++) { for (int x = 0; x < rgbaStride; x += 4) { (*rgbaArrayBuffer)[(w * 4 * y) + x + 0] = (byte)(y % r); (*rgbaArrayBuffer)[(w * 4 * y) + x + 1] = (byte)(y % g); (*rgbaArrayBuffer)[(w * 4 * y) + x + 2] = (byte)(y % b); (*rgbaArrayBuffer)[(w * 4 * y) + x + 3] = 255; } } } public void Start() { frameIteratorThread = new Thread(VideoFrameIterator); frameIteratorThread.Start(); } public void Stop() { try { if (frameIteratorThread != null) { stopFrameIterator = true; frameIteratorThread.Join(); frameIteratorThread = null; stopFrameIterator = false; } } catch (Exception ex) { string msg = ex.Message; } } }
Inscreva-se no
VideoStream.StateChanged
delegado. Esse evento informa o estado do fluxo atual. Não envie quadros se o estado não for igual aVideoStreamState.Started
.private VideoFrameSender videoFrameSender; rawOutgoingVideoStream.StateChanged += (object sender, VideoStreamStateChangedEventArgs args) => { CallVideoStream callVideoStream = args.Stream; switch (callVideoStream.State) { case VideoStreamState.Available: // VideoStream has been attached to the call var frameKind = RawVideoFrameKind.Buffer; // Use the frameKind you prefer //var frameKind = RawVideoFrameKind.Texture; videoFrameSender = new VideoFrameSender(frameKind, rawOutgoingVideoStream); break; case VideoStreamState.Started: // Start sending frames videoFrameSender.Start(); break; case VideoStreamState.Stopped: // Stop sending frames videoFrameSender.Stop(); break; } };
Vídeo de compartilhamento de tela
Como o sistema Windows gera os quadros, você deve implementar seu próprio serviço de primeiro plano para capturar os quadros e enviá-los usando a API de Chamada dos Serviços de Comunicação do Azure.
Resoluções de vídeo suportadas
Relação de aspeto | Resolução | FPS máximo |
---|---|---|
Qualquer coisa | Qualquer coisa até 1080p | 30 |
Etapas para criar um fluxo de vídeo de compartilhamento de tela
- Crie uma matriz usando
VideoFormat
o VideoStreamPixelFormat suportado pelo SDK. Quando vários formatos estão disponíveis, a ordem dos formatos na lista não influencia ou prioriza qual deles é usado. Os critérios para a seleção do formato são baseados em fatores externos, como a largura de banda da rede.var videoStreamFormat = new VideoStreamFormat { Width = 1280, // Width and height can be used for ScreenShareOutgoingVideoStream for custom resolutions or use one of the predefined values inside VideoStreamResolution Height = 720, //Resolution = VideoStreamResolution.P720, PixelFormat = VideoStreamPixelFormat.Rgba, FramesPerSecond = 30, Stride1 = 1280 * 4 // It is times 4 because RGBA is a 32-bit format. }; VideoStreamFormat[] videoStreamFormats = { videoStreamFormat };
- Crie
RawOutgoingVideoStreamOptions
e definaVideoFormats
com o objeto criado anteriormente.var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions { Formats = videoStreamFormats };
- Crie uma instância de
VirtualOutgoingVideoStream
usando aRawOutgoingVideoStreamOptions
instância que você criou anteriormente.var rawOutgoingVideoStream = new ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions);
- Capture e envie o quadro de vídeo da seguinte maneira.
private async Task SendRawVideoFrame() { RawVideoFrame videoFrame = null; switch (rawVideoFrameKind) //it depends on the frame kind you want to send { case RawVideoFrameKind.Buffer: MemoryBuffer memoryBuffer = // Fill it with the content you got from the Windows APIs videoFrame = new RawVideoFrameBuffer() { Buffers = memoryBuffer // The number of buffers depends on the VideoStreamPixelFormat StreamFormat = rawOutgoingVideoStream.Format }; break; case RawVideoFrameKind.Texture: MediaStreamSample mediaStreamSample = // Fill it with the content you got from the Windows APIs videoFrame = new RawVideoFrameTexture() { Texture = mediaStreamSample, // Texture only receive planar buffers StreamFormat = rawOutgoingVideoStream.Format }; break; } try { using (videoFrame) { await rawOutgoingVideoStream.SendRawVideoFrameAsync(videoFrame); } } catch (Exception ex) { string msg = ex.Message; } try { int delayBetweenFrames = (int)(1000.0 / rawOutgoingVideoStream.Format.FramesPerSecond); await Task.Delay(delayBetweenFrames); } catch (Exception ex) { string msg = ex.Message; } }
Vídeo de entrada Raw
Este recurso lhe dá acesso aos quadros de vídeo dentro do 's, IncomingVideoStream
a fim de manipular esses fluxos localmente
- Criar uma instância desse conjunto por meio
JoinCallOptions
daIncomingVideoOptions
configuraçãoVideoStreamKind.RawIncoming
var frameKind = RawVideoFrameKind.Buffer; // Use the frameKind you prefer to receive var incomingVideoOptions = new IncomingVideoOptions { StreamKind = VideoStreamKind.RawIncoming, FrameKind = frameKind }; var joinCallOptions = new JoinCallOptions { IncomingVideoOptions = incomingVideoOptions };
- Depois de receber um
ParticipantsUpdatedEventArgs
evento, anexeRemoteParticipant.VideoStreamStateChanged
o delegado. Esse evento informa o estado dosIncomingVideoStream
objetos.private List<RemoteParticipant> remoteParticipantList; private void OnRemoteParticipantsUpdated(object sender, ParticipantsUpdatedEventArgs args) { foreach (RemoteParticipant remoteParticipant in args.AddedParticipants) { IReadOnlyList<IncomingVideoStream> incomingVideoStreamList = remoteParticipant.IncomingVideoStreams; // Check if there are IncomingVideoStreams already before attaching the delegate foreach (IncomingVideoStream incomingVideoStream in incomingVideoStreamList) { OnRawIncomingVideoStreamStateChanged(incomingVideoStream); } remoteParticipant.VideoStreamStateChanged += OnVideoStreamStateChanged; remoteParticipantList.Add(remoteParticipant); // If the RemoteParticipant ref is not kept alive the VideoStreamStateChanged events are going to be missed } foreach (RemoteParticipant remoteParticipant in args.RemovedParticipants) { remoteParticipant.VideoStreamStateChanged -= OnVideoStreamStateChanged; remoteParticipantList.Remove(remoteParticipant); } } private void OnVideoStreamStateChanged(object sender, VideoStreamStateChangedEventArgs args) { CallVideoStream callVideoStream = args.Stream; OnRawIncomingVideoStreamStateChanged(callVideoStream as RawIncomingVideoStream); } private void OnRawIncomingVideoStreamStateChanged(RawIncomingVideoStream rawIncomingVideoStream) { switch (incomingVideoStream.State) { case VideoStreamState.Available: // There is a new IncomingVideoStream rawIncomingVideoStream.RawVideoFrameReceived += OnVideoFrameReceived; rawIncomingVideoStream.Start(); break; case VideoStreamState.Started: // Will start receiving video frames break; case VideoStreamState.Stopped: // Will stop receiving video frames break; case VideoStreamState.NotAvailable: // The IncomingVideoStream should not be used anymore rawIncomingVideoStream.RawVideoFrameReceived -= OnVideoFrameReceived; break; } }
- No momento, o
IncomingVideoStream
temVideoStreamState.Available
estado anexarRawIncomingVideoStream.RawVideoFrameReceived
delegado, como mostrado na etapa anterior. Isso fornece os novosRawVideoFrame
objetos.private async void OnVideoFrameReceived(object sender, RawVideoFrameReceivedEventArgs args) { RawVideoFrame videoFrame = args.Frame; switch (videoFrame.Kind) // The type will be whatever was configured on the IncomingVideoOptions { case RawVideoFrameKind.Buffer: // Render/Modify/Save the video frame break; case RawVideoFrameKind.Texture: // Render/Modify/Save the video frame break; } }
Neste início rápido, você aprenderá a implementar o acesso bruto a mídia usando o SDK de Chamada dos Serviços de Comunicação do Azure para Android.
O SDK de Chamada dos Serviços de Comunicação do Azure oferece APIs que permitem que os aplicativos gerem seus próprios quadros de vídeo para enviar aos participantes remotos de uma chamada.
Este guia de início rápido baseia-se em Guia de início rápido: adicione chamadas de vídeo 1:1 ao seu aplicativo para Android.
Acesso RawAudio
O acesso à mídia de áudio bruto dá acesso ao fluxo de áudio de entrada da chamada, juntamente com a capacidade de visualizar e enviar fluxos de áudio de saída personalizados durante uma chamada.
Enviar áudio de saída Raw
Crie um objeto options especificando as propriedades de fluxo bruto que queremos enviar.
RawOutgoingAudioStreamProperties outgoingAudioProperties = new RawOutgoingAudioStreamProperties()
.setAudioFormat(AudioStreamFormat.PCM16_BIT)
.setSampleRate(AudioStreamSampleRate.HZ44100)
.setChannelMode(AudioStreamChannelMode.STEREO)
.setBufferDuration(AudioStreamBufferDuration.IN_MS20);
RawOutgoingAudioStreamOptions outgoingAudioStreamOptions = new RawOutgoingAudioStreamOptions()
.setProperties(outgoingAudioProperties);
Crie um RawOutgoingAudioStream
e anexe-o para participar das opções de chamada e o fluxo é iniciado automaticamente quando a chamada é conectada.
JoinCallOptions options = JoinCallOptions() // or StartCallOptions()
OutgoingAudioOptions outgoingAudioOptions = new OutgoingAudioOptions();
RawOutgoingAudioStream rawOutgoingAudioStream = new RawOutgoingAudioStream(outgoingAudioStreamOptions);
outgoingAudioOptions.setStream(rawOutgoingAudioStream);
options.setOutgoingAudioOptions(outgoingAudioOptions);
// Start or Join call with those call options.
Anexar fluxo a uma chamada
Ou você também pode anexar o fluxo a uma instância existente Call
:
CompletableFuture<Void> result = call.startAudio(context, rawOutgoingAudioStream);
Comece a enviar amostras brutas
Só podemos começar a enviar dados quando o estado do fluxo for AudioStreamState.STARTED
.
Para observar a alteração do estado do fluxo de áudio, adicione um ouvinte ao OnStateChangedListener
evento.
private void onStateChanged(PropertyChangedEvent propertyChangedEvent) {
// When value is `AudioStreamState.STARTED` we'll be able to send audio samples.
}
rawOutgoingAudioStream.addOnStateChangedListener(this::onStateChanged)
Quando o fluxo começou, podemos começar a enviar java.nio.ByteBuffer
amostras de áudio para a chamada.
O formato do buffer de áudio deve corresponder às propriedades de fluxo especificadas.
Thread thread = new Thread(){
public void run() {
RawAudioBuffer buffer;
Calendar nextDeliverTime = Calendar.getInstance();
while (true)
{
nextDeliverTime.add(Calendar.MILLISECOND, 20);
byte data[] = new byte[outgoingAudioStream.getExpectedBufferSizeInBytes()];
//can grab microphone data from AudioRecord
ByteBuffer dataBuffer = ByteBuffer.allocateDirect(outgoingAudioStream.getExpectedBufferSizeInBytes());
dataBuffer.rewind();
buffer = new RawAudioBuffer(dataBuffer);
outgoingAudioStream.sendOutgoingAudioBuffer(buffer);
long wait = nextDeliverTime.getTimeInMillis() - Calendar.getInstance().getTimeInMillis();
if (wait > 0)
{
try {
Thread.sleep(wait);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
};
thread.start();
Receber áudio Raw Incoming
Também podemos receber as amostras de fluxo de áudio da chamada como java.nio.ByteBuffer
se quiséssemos processar o áudio antes da reprodução.
Crie um RawIncomingAudioStreamOptions
objeto especificando as propriedades de fluxo bruto que queremos receber.
RawIncomingAudioStreamOptions options = new RawIncomingAudioStreamOptions();
RawIncomingAudioStreamProperties properties = new RawIncomingAudioStreamProperties()
.setAudioFormat(AudioStreamFormat.PCM16_BIT)
.setSampleRate(AudioStreamSampleRate.HZ44100)
.setChannelMode(AudioStreamChannelMode.STEREO);
options.setProperties(properties);
Criar um RawIncomingAudioStream
e anexá-lo para participar de opções de chamada
JoinCallOptions options = JoinCallOptions() // or StartCallOptions()
IncomingAudioOptions incomingAudioOptions = new IncomingAudioOptions();
RawIncomingAudioStream rawIncomingAudioStream = new RawIncomingAudioStream(audioStreamOptions);
incomingAudioOptions.setStream(rawIncomingAudioStream);
options.setIncomingAudioOptions(incomingAudioOptions);
Ou também podemos anexar o fluxo a uma instância existente Call
:
CompletableFuture<Void> result = call.startAudio(context, rawIncomingAudioStream);
Para começar a receber buffers de áudio brutos do fluxo de entrada, adicione ouvintes ao estado do fluxo de entrada e aos eventos recebidos do buffer.
private void onStateChanged(PropertyChangedEvent propertyChangedEvent) {
// When value is `AudioStreamState.STARTED` we'll be able to receive samples.
}
private void onMixedAudioBufferReceived(IncomingMixedAudioEvent incomingMixedAudioEvent) {
// Received a raw audio buffers(java.nio.ByteBuffer).
}
rawIncomingAudioStream.addOnStateChangedListener(this::onStateChanged);
rawIncomingAudioStream.addMixedAudioBufferReceivedListener(this::onMixedAudioBufferReceived);
Também é importante lembrar de interromper o fluxo de áudio na instância de chamada Call
atual:
CompletableFuture<Void> result = call.stopAudio(context, rawIncomingAudioStream);
Acesso ao RawVideo
Como o aplicativo gera os quadros de vídeo, o aplicativo deve informar o SDK de Chamada dos Serviços de Comunicação do Azure sobre os formatos de vídeo que o aplicativo pode gerar. Essas informações permitem que o SDK de Chamada dos Serviços de Comunicação do Azure escolha a melhor configuração de formato de vídeo para as condições de rede naquele momento.
Vídeo Virtual
Resoluções de vídeo suportadas
Relação de aspeto | Resolução | FPS máximo |
---|---|---|
16x9 | 1080p | 30 |
16x9 | 720p | 30 |
16x9 | 540p | 30 |
16x9 | 480p | 30 |
16x9 | 360p | 30 |
16x9 | 270p | 15 |
16x9 | 240p | 15 |
16x9 | 180p | 15 |
4x3 | VGA (640x480) | 30 |
4x3 | 424x320 | 15 |
4x3 | QVGA (320x240) | 15 |
4x3 | 212x160 | 15 |
Crie uma matriz usando
VideoFormat
o VideoStreamPixelFormat suportado pelo SDK.Quando vários formatos estão disponíveis, a ordem dos formatos na lista não influencia ou prioriza qual deles é usado. Os critérios para a seleção do formato são baseados em fatores externos, como a largura de banda da rede.
VideoStreamFormat videoStreamFormat = new VideoStreamFormat(); videoStreamFormat.setResolution(VideoStreamResolution.P360); videoStreamFormat.setPixelFormat(VideoStreamPixelFormat.RGBA); videoStreamFormat.setFramesPerSecond(framerate); videoStreamFormat.setStride1(w * 4); // It is times 4 because RGBA is a 32-bit format List<VideoStreamFormat> videoStreamFormats = new ArrayList<>(); videoStreamFormats.add(videoStreamFormat);
Crie
RawOutgoingVideoStreamOptions
e definaFormats
com o objeto criado anteriormente.RawOutgoingVideoStreamOptions rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions(); rawOutgoingVideoStreamOptions.setFormats(videoStreamFormats);
Crie uma instância de
VirtualOutgoingVideoStream
usando aRawOutgoingVideoStreamOptions
instância que você criou anteriormente.VirtualOutgoingVideoStream rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
Inscreva-se no
RawOutgoingVideoStream.addOnFormatChangedListener
delegado. Este evento informa sempre que o foi alterado aVideoStreamFormat
partir de um dos formatos de vídeo fornecidos na lista.virtualOutgoingVideoStream.addOnFormatChangedListener((VideoStreamFormatChangedEvent args) -> { VideoStreamFormat videoStreamFormat = args.Format; });
Crie uma instância da seguinte classe auxiliar para gerar 's aleatórios
RawVideoFrame
usandoVideoStreamPixelFormat.RGBA
public class VideoFrameSender { private RawOutgoingVideoStream rawOutgoingVideoStream; private Thread frameIteratorThread; private Random random = new Random(); private volatile boolean stopFrameIterator = false; public VideoFrameSender(RawOutgoingVideoStream rawOutgoingVideoStream) { this.rawOutgoingVideoStream = rawOutgoingVideoStream; } public void VideoFrameIterator() { while (!stopFrameIterator) { if (rawOutgoingVideoStream != null && rawOutgoingVideoStream.getFormat() != null && rawOutgoingVideoStream.getState() == VideoStreamState.STARTED) { SendRandomVideoFrameRGBA(); } } } private void SendRandomVideoFrameRGBA() { int rgbaCapacity = rawOutgoingVideoStream.getFormat().getWidth() * rawOutgoingVideoStream.getFormat().getHeight() * 4; RawVideoFrame videoFrame = GenerateRandomVideoFrameBuffer(rawOutgoingVideoStream.getFormat(), rgbaCapacity); try { rawOutgoingVideoStream.sendRawVideoFrame(videoFrame).get(); int delayBetweenFrames = (int)(1000.0 / rawOutgoingVideoStream.getFormat().getFramesPerSecond()); Thread.sleep(delayBetweenFrames); } catch (Exception ex) { String msg = ex.getMessage(); } finally { videoFrame.close(); } } private RawVideoFrame GenerateRandomVideoFrameBuffer(VideoStreamFormat videoStreamFormat, int rgbaCapacity) { ByteBuffer rgbaBuffer = ByteBuffer.allocateDirect(rgbaCapacity); // Only allocateDirect ByteBuffers are allowed rgbaBuffer.order(ByteOrder.nativeOrder()); GenerateRandomVideoFrame(rgbaBuffer, rgbaCapacity); RawVideoFrameBuffer videoFrameBuffer = new RawVideoFrameBuffer(); videoFrameBuffer.setBuffers(Arrays.asList(rgbaBuffer)); videoFrameBuffer.setStreamFormat(videoStreamFormat); return videoFrameBuffer; } private void GenerateRandomVideoFrame(ByteBuffer rgbaBuffer, int rgbaCapacity) { int w = rawOutgoingVideoStream.getFormat().getWidth(); int h = rawOutgoingVideoStream.getFormat().getHeight(); byte rVal = (byte)random.nextInt(255); byte gVal = (byte)random.nextInt(255); byte bVal = (byte)random.nextInt(255); byte aVal = (byte)255; byte[] rgbaArrayBuffer = new byte[rgbaCapacity]; int rgbaStride = w * 4; for (int y = 0; y < h; y++) { for (int x = 0; x < rgbaStride; x += 4) { rgbaArrayBuffer[(w * 4 * y) + x + 0] = rVal; rgbaArrayBuffer[(w * 4 * y) + x + 1] = gVal; rgbaArrayBuffer[(w * 4 * y) + x + 2] = bVal; rgbaArrayBuffer[(w * 4 * y) + x + 3] = aVal; } } rgbaBuffer.put(rgbaArrayBuffer); rgbaBuffer.rewind(); } public void Start() { frameIteratorThread = new Thread(this::VideoFrameIterator); frameIteratorThread.start(); } public void Stop() { try { if (frameIteratorThread != null) { stopFrameIterator = true; frameIteratorThread.join(); frameIteratorThread = null; stopFrameIterator = false; } } catch (InterruptedException ex) { String msg = ex.getMessage(); } } }
Inscreva-se no
VideoStream.addOnStateChangedListener
delegado. Este delegado informa o estado do fluxo atual. Não envie quadros se o estado não for igual aVideoStreamState.STARTED
.private VideoFrameSender videoFrameSender; rawOutgoingVideoStream.addOnStateChangedListener((VideoStreamStateChangedEvent args) -> { CallVideoStream callVideoStream = args.getStream(); switch (callVideoStream.getState()) { case AVAILABLE: videoFrameSender = new VideoFrameSender(rawOutgoingVideoStream); break; case STARTED: // Start sending frames videoFrameSender.Start(); break; case STOPPED: // Stop sending frames videoFrameSender.Stop(); break; } });
Vídeo ScreenShare
Como o sistema Windows gera os quadros, você deve implementar seu próprio serviço de primeiro plano para capturar os quadros e enviá-los usando a API de Chamada dos Serviços de Comunicação do Azure.
Resoluções de vídeo suportadas
Relação de aspeto | Resolução | FPS máximo |
---|---|---|
Qualquer coisa | Qualquer coisa até 1080p | 30 |
Etapas para criar um fluxo de vídeo de compartilhamento de tela
Crie uma matriz usando
VideoFormat
o VideoStreamPixelFormat suportado pelo SDK.Quando vários formatos estão disponíveis, a ordem dos formatos na lista não influencia ou prioriza qual deles é usado. Os critérios para a seleção do formato são baseados em fatores externos, como a largura de banda da rede.
VideoStreamFormat videoStreamFormat = new VideoStreamFormat(); videoStreamFormat.setWidth(1280); // Width and height can be used for ScreenShareOutgoingVideoStream for custom resolutions or use one of the predefined values inside VideoStreamResolution videoStreamFormat.setHeight(720); //videoStreamFormat.setResolution(VideoStreamResolution.P360); videoStreamFormat.setPixelFormat(VideoStreamPixelFormat.RGBA); videoStreamFormat.setFramesPerSecond(framerate); videoStreamFormat.setStride1(w * 4); // It is times 4 because RGBA is a 32-bit format List<VideoStreamFormat> videoStreamFormats = new ArrayList<>(); videoStreamFormats.add(videoStreamFormat);
Crie
RawOutgoingVideoStreamOptions
e definaVideoFormats
com o objeto criado anteriormente.RawOutgoingVideoStreamOptions rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions(); rawOutgoingVideoStreamOptions.setFormats(videoStreamFormats);
Crie uma instância de
VirtualOutgoingVideoStream
usando aRawOutgoingVideoStreamOptions
instância que você criou anteriormente.ScreenShareOutgoingVideoStream rawOutgoingVideoStream = new ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions);
Capture e envie o quadro de vídeo da seguinte maneira.
private void SendRawVideoFrame() { ByteBuffer byteBuffer = // Fill it with the content you got from the Windows APIs RawVideoFrameBuffer videoFrame = new RawVideoFrameBuffer(); videoFrame.setBuffers(Arrays.asList(byteBuffer)); // The number of buffers depends on the VideoStreamPixelFormat videoFrame.setStreamFormat(rawOutgoingVideoStream.getFormat()); try { rawOutgoingVideoStream.sendRawVideoFrame(videoFrame).get(); } catch (Exception ex) { String msg = ex.getMessage(); } finally { videoFrame.close(); } }
Vídeo de entrada Raw
Este recurso lhe dá acesso aos quadros de vídeo dentro dos objetos, IncomingVideoStream
a fim de manipular esses quadros localmente
Criar uma instância desse conjunto por meio
JoinCallOptions
daIncomingVideoOptions
configuraçãoVideoStreamKind.RawIncoming
IncomingVideoOptions incomingVideoOptions = new IncomingVideoOptions() .setStreamType(VideoStreamKind.RAW_INCOMING); JoinCallOptions joinCallOptions = new JoinCallOptions() .setIncomingVideoOptions(incomingVideoOptions);
Depois de receber um
ParticipantsUpdatedEventArgs
evento, anexeRemoteParticipant.VideoStreamStateChanged
o delegado. Esse evento informa o estado doIncomingVideoStream
objeto.private List<RemoteParticipant> remoteParticipantList; private void OnRemoteParticipantsUpdated(ParticipantsUpdatedEventArgs args) { for (RemoteParticipant remoteParticipant : args.getAddedParticipants()) { List<IncomingVideoStream> incomingVideoStreamList = remoteParticipant.getIncomingVideoStreams(); // Check if there are IncomingVideoStreams already before attaching the delegate for (IncomingVideoStream incomingVideoStream : incomingVideoStreamList) { OnRawIncomingVideoStreamStateChanged(incomingVideoStream); } remoteParticipant.addOnVideoStreamStateChanged(this::OnVideoStreamStateChanged); remoteParticipantList.add(remoteParticipant); // If the RemoteParticipant ref is not kept alive the VideoStreamStateChanged events are going to be missed } for (RemoteParticipant remoteParticipant : args.getRemovedParticipants()) { remoteParticipant.removeOnVideoStreamStateChanged(this::OnVideoStreamStateChanged); remoteParticipantList.remove(remoteParticipant); } } private void OnVideoStreamStateChanged(object sender, VideoStreamStateChangedEventArgs args) { CallVideoStream callVideoStream = args.getStream(); OnRawIncomingVideoStreamStateChanged((RawIncomingVideoStream) callVideoStream); } private void OnRawIncomingVideoStreamStateChanged(RawIncomingVideoStream rawIncomingVideoStream) { switch (incomingVideoStream.State) { case AVAILABLE: // There is a new IncomingvideoStream rawIncomingVideoStream.addOnRawVideoFrameReceived(this::OnVideoFrameReceived); rawIncomingVideoStream.Start(); break; case STARTED: // Will start receiving video frames break; case STOPPED: // Will stop receiving video frames break; case NOT_AVAILABLE: // The IncomingvideoStream should not be used anymore rawIncomingVideoStream.removeOnRawVideoFrameReceived(this::OnVideoFrameReceived); break; } }
No momento, o
IncomingVideoStream
temVideoStreamState.Available
estado anexarRawIncomingVideoStream.RawVideoFrameReceived
delegado, como mostrado na etapa anterior. Esse delegado fornece os novosRawVideoFrame
objetos.private void OnVideoFrameReceived(RawVideoFrameReceivedEventArgs args) { // Render/Modify/Save the video frame RawVideoFrameBuffer videoFrame = (RawVideoFrameBuffer) args.getFrame(); }
Neste início rápido, você aprenderá a implementar o acesso bruto a mídia usando o SDK de Chamada dos Serviços de Comunicação do Azure para iOS.
O SDK de Chamada dos Serviços de Comunicação do Azure oferece APIs que permitem que os aplicativos gerem seus próprios quadros de vídeo para enviar aos participantes remotos de uma chamada.
Este guia de início rápido baseia-se em Guia de início rápido: adicione chamadas de vídeo 1:1 ao seu aplicativo para iOS.
Acesso RawAudio
O acesso a mídia de áudio bruto dá acesso ao fluxo de áudio da chamada recebida, juntamente com a capacidade de visualizar e enviar fluxos de áudio de saída personalizados durante uma chamada.
Enviar áudio de saída Raw
Crie um objeto options especificando as propriedades de fluxo bruto que queremos enviar.
let outgoingAudioStreamOptions = RawOutgoingAudioStreamOptions()
let properties = RawOutgoingAudioStreamProperties()
properties.sampleRate = .hz44100
properties.bufferDuration = .inMs20
properties.channelMode = .mono
properties.format = .pcm16Bit
outgoingAudioStreamOptions.properties = properties
Crie um RawOutgoingAudioStream
e anexe-o para participar das opções de chamada e o fluxo é iniciado automaticamente quando a chamada é conectada.
let options = JoinCallOptions() // or StartCallOptions()
let outgoingAudioOptions = OutgoingAudioOptions()
self.rawOutgoingAudioStream = RawOutgoingAudioStream(rawOutgoingAudioStreamOptions: outgoingAudioStreamOptions)
outgoingAudioOptions.stream = self.rawOutgoingAudioStream
options.outgoingAudioOptions = outgoingAudioOptions
// Start or Join call passing the options instance.
Anexar fluxo a uma chamada
Ou você também pode anexar o fluxo a uma instância existente Call
:
call.startAudio(stream: self.rawOutgoingAudioStream) { error in
// Stream attached to `Call`.
}
Comece a enviar amostras brutas
Só podemos começar a enviar dados quando o estado do fluxo for AudioStreamState.started
.
Para observar a mudança de estado do fluxo de áudio, implementamos o RawOutgoingAudioStreamDelegate
. E defini-lo como o delegado de fluxo.
func rawOutgoingAudioStream(_ rawOutgoingAudioStream: RawOutgoingAudioStream,
didChangeState args: AudioStreamStateChangedEventArgs) {
// When value is `AudioStreamState.started` we will be able to send audio samples.
}
self.rawOutgoingAudioStream.delegate = DelegateImplementer()
ou utilizar com base no fecho
self.rawOutgoingAudioStream.events.onStateChanged = { args in
// When value is `AudioStreamState.started` we will be able to send audio samples.
}
Quando o fluxo começou, podemos começar a enviar AVAudioPCMBuffer
amostras de áudio para a chamada.
O formato do buffer de áudio deve corresponder às propriedades de fluxo especificadas.
protocol SamplesProducer {
func produceSample(_ currentSample: Int,
options: RawOutgoingAudioStreamOptions) -> AVAudioPCMBuffer
}
// Let's use a simple Tone data producer as example.
// Producing PCM buffers.
func produceSamples(_ currentSample: Int,
stream: RawOutgoingAudioStream,
options: RawOutgoingAudioStreamOptions) -> AVAudioPCMBuffer {
let sampleRate = options.properties.sampleRate
let channelMode = options.properties.channelMode
let bufferDuration = options.properties.bufferDuration
let numberOfChunks = UInt32(1000 / bufferDuration.value)
let bufferFrameSize = UInt32(sampleRate.valueInHz) / numberOfChunks
let frequency = 400
guard let format = AVAudioFormat(commonFormat: .pcmFormatInt16,
sampleRate: sampleRate.valueInHz,
channels: channelMode.channelCount,
interleaved: channelMode == .stereo) else {
fatalError("Failed to create PCM Format")
}
guard let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: bufferFrameSize) else {
fatalError("Failed to create PCM buffer")
}
buffer.frameLength = bufferFrameSize
let factor: Double = ((2 as Double) * Double.pi) / (sampleRate.valueInHz/Double(frequency))
var interval = 0
for sampleIdx in 0..<Int(buffer.frameCapacity * channelMode.channelCount) {
let sample = sin(factor * Double(currentSample + interval))
// Scale to maximum amplitude. Int16.max is 37,767.
let value = Int16(sample * Double(Int16.max))
guard let underlyingByteBuffer = buffer.mutableAudioBufferList.pointee.mBuffers.mData else {
continue
}
underlyingByteBuffer.assumingMemoryBound(to: Int16.self).advanced(by: sampleIdx).pointee = value
interval += channelMode == .mono ? 2 : 1
}
return buffer
}
final class RawOutgoingAudioSender {
let stream: RawOutgoingAudioStream
let options: RawOutgoingAudioStreamOptions
let producer: SamplesProducer
private var timer: Timer?
private var currentSample: Int = 0
private var currentTimestamp: Int64 = 0
init(stream: RawOutgoingAudioStream,
options: RawOutgoingAudioStreamOptions,
producer: SamplesProducer) {
self.stream = stream
self.options = options
self.producer = producer
}
func start() {
let properties = self.options.properties
let interval = properties.bufferDuration.timeInterval
let channelCount = AVAudioChannelCount(properties.channelMode.channelCount)
let format = AVAudioFormat(commonFormat: .pcmFormatInt16,
sampleRate: properties.sampleRate.valueInHz,
channels: channelCount,
interleaved: channelCount > 1)!
self.timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
guard let self = self else { return }
let sample = self.producer.produceSamples(self.currentSample, options: self.options)
let rawBuffer = RawAudioBuffer()
rawBuffer.buffer = sample
rawBuffer.timestampInTicks = self.currentTimestamp
self.stream.send(buffer: rawBuffer, completionHandler: { error in
if let error = error {
// Handle possible error.
}
})
self.currentTimestamp += Int64(properties.bufferDuration.value)
self.currentSample += 1
}
}
func stop() {
self.timer?.invalidate()
self.timer = nil
}
deinit {
stop()
}
}
Também é importante lembrar de interromper o fluxo de áudio na instância de chamada Call
atual:
call.stopAudio(stream: self.rawOutgoingAudioStream) { error in
// Stream detached from `Call` and stopped.
}
Captura de amostras de microfone
Usando o AVAudioEngine
da Apple, podemos capturar quadros de microfone tocando no nó de entrada do mecanismo de áudio. E capturando os dados do microfone e sendo capazes de usar a funcionalidade de áudio bruto, somos capazes de processar o áudio antes de enviá-lo para uma chamada.
import AVFoundation
import AzureCommunicationCalling
enum MicrophoneSenderError: Error {
case notMatchingFormat
}
final class MicrophoneDataSender {
private let stream: RawOutgoingAudioStream
private let properties: RawOutgoingAudioStreamProperties
private let format: AVAudioFormat
private let audioEngine: AVAudioEngine = AVAudioEngine()
init(properties: RawOutgoingAudioStreamProperties) throws {
// This can be different depending on which device we are running or value set for
// `try AVAudioSession.sharedInstance().setPreferredSampleRate(...)`.
let nodeFormat = self.audioEngine.inputNode.outputFormat(forBus: 0)
let matchingSampleRate = AudioSampleRate.allCases.first(where: { $0.valueInHz == nodeFormat.sampleRate })
guard let inputNodeSampleRate = matchingSampleRate else {
throw MicrophoneSenderError.notMatchingFormat
}
// Override the sample rate to one that matches audio session (Audio engine input node frequency).
properties.sampleRate = inputNodeSampleRate
let options = RawOutgoingAudioStreamOptions()
options.properties = properties
self.stream = RawOutgoingAudioStream(rawOutgoingAudioStreamOptions: options)
let channelCount = AVAudioChannelCount(properties.channelMode.channelCount)
self.format = AVAudioFormat(commonFormat: .pcmFormatInt16,
sampleRate: properties.sampleRate.valueInHz,
channels: channelCount,
interleaved: channelCount > 1)!
self.properties = properties
}
func start() throws {
guard !self.audioEngine.isRunning else {
return
}
// Install tap documentations states that we can get between 100 and 400 ms of data.
let framesFor100ms = AVAudioFrameCount(self.format.sampleRate * 0.1)
// Note that some formats may not be allowed by `installTap`, so we have to specify the
// correct properties.
self.audioEngine.inputNode.installTap(onBus: 0, bufferSize: framesFor100ms,
format: self.format) { [weak self] buffer, _ in
guard let self = self else { return }
let rawBuffer = RawAudioBuffer()
rawBuffer.buffer = buffer
// Although we specified either 10ms or 20ms, we allow sending up to 100ms of data
// as long as it can be evenly divided by the specified size.
self.stream.send(buffer: rawBuffer) { error in
if let error = error {
// Handle error
}
}
}
try audioEngine.start()
}
func stop() {
audioEngine.stop()
}
}
Nota
A taxa de amostragem do nó de entrada do mecanismo de áudio assume como padrão um >valor da taxa de amostragem preferida para a sessão de áudio compartilhada. Portanto, não podemos instalar o toque nesse nó usando um valor diferente.
Portanto, temos que garantir que a RawOutgoingStream
taxa de amostragem de propriedades corresponda àquela que obtemos de amostras de toque em microfone ou converter os buffers de toque para o formato que corresponde ao que é esperado no fluxo de saída.
Com esta pequena amostra, aprendemos como podemos capturar os dados do microfone AVAudioEngine
e enviar essas amostras para uma chamada usando o recurso de áudio de saída bruto.
Receber áudio Raw Incoming
Também podemos receber as amostras de fluxo de áudio da chamada como AVAudioPCMBuffer
se quiséssemos processar o áudio antes da reprodução.
Crie um RawIncomingAudioStreamOptions
objeto especificando as propriedades de fluxo bruto que queremos receber.
let options = RawIncomingAudioStreamOptions()
let properties = RawIncomingAudioStreamProperties()
properties.format = .pcm16Bit
properties.sampleRate = .hz44100
properties.channelMode = .stereo
options.properties = properties
Criar um RawOutgoingAudioStream
e anexá-lo para participar de opções de chamada
let options = JoinCallOptions() // or StartCallOptions()
let incomingAudioOptions = IncomingAudioOptions()
self.rawIncomingStream = RawIncomingAudioStream(rawIncomingAudioStreamOptions: audioStreamOptions)
incomingAudioOptions.stream = self.rawIncomingStream
options.incomingAudioOptions = incomingAudioOptions
Ou também podemos anexar o fluxo a uma instância existente Call
:
call.startAudio(stream: self.rawIncomingStream) { error in
// Stream attached to `Call`.
}
Para começar a receber o buffer de áudio bruto do fluxo de entrada, implemente o RawIncomingAudioStreamDelegate
:
class RawIncomingReceiver: NSObject, RawIncomingAudioStreamDelegate {
func rawIncomingAudioStream(_ rawIncomingAudioStream: RawIncomingAudioStream,
didChangeState args: AudioStreamStateChangedEventArgs) {
// To be notified when stream started and stopped.
}
func rawIncomingAudioStream(_ rawIncomingAudioStream: RawIncomingAudioStream,
mixedAudioBufferReceived args: IncomingMixedAudioEventArgs) {
// Receive raw audio buffers(AVAudioPCMBuffer) and process using AVAudioEngine API's.
}
}
self.rawIncomingStream.delegate = RawIncomingReceiver()
ou
rawIncomingAudioStream.events.mixedAudioBufferReceived = { args in
// Receive raw audio buffers(AVAudioPCMBuffer) and process them using AVAudioEngine API's.
}
rawIncomingAudioStream.events.onStateChanged = { args in
// To be notified when stream started and stopped.
}
Acesso ao RawVideo
Como o aplicativo gera os quadros de vídeo, o aplicativo deve informar o SDK de Chamada dos Serviços de Comunicação do Azure sobre os formatos de vídeo que o aplicativo pode gerar. Essas informações permitem que o SDK de Chamada dos Serviços de Comunicação do Azure escolha a melhor configuração de formato de vídeo para as condições de rede naquele momento.
Vídeo Virtual
Resoluções de vídeo suportadas
Relação de aspeto | Resolução | FPS máximo |
---|---|---|
16x9 | 1080p | 30 |
16x9 | 720p | 30 |
16x9 | 540p | 30 |
16x9 | 480p | 30 |
16x9 | 360p | 30 |
16x9 | 270p | 15 |
16x9 | 240p | 15 |
16x9 | 180p | 15 |
4x3 | VGA (640x480) | 30 |
4x3 | 424x320 | 15 |
4x3 | QVGA (320x240) | 15 |
4x3 | 212x160 | 15 |
Crie uma matriz usando
VideoFormat
o VideoStreamPixelFormat suportado pelo SDK. Quando vários formatos estão disponíveis, a ordem dos formatos na lista não influencia ou prioriza qual deles é usado. Os critérios para a seleção do formato são baseados em fatores externos, como a largura de banda da rede.var videoStreamFormat = VideoStreamFormat() videoStreamFormat.resolution = VideoStreamResolution.p360 videoStreamFormat.pixelFormat = VideoStreamPixelFormat.nv12 videoStreamFormat.framesPerSecond = framerate videoStreamFormat.stride1 = w // w is the resolution width videoStreamFormat.stride2 = w / 2 // w is the resolution width var videoStreamFormats: [VideoStreamFormat] = [VideoStreamFormat]() videoStreamFormats.append(videoStreamFormat)
Crie
RawOutgoingVideoStreamOptions
e defina formatos com o objeto criado anteriormente.var rawOutgoingVideoStreamOptions = RawOutgoingVideoStreamOptions() rawOutgoingVideoStreamOptions.formats = videoStreamFormats
Crie uma instância de
VirtualOutgoingVideoStream
usando aRawOutgoingVideoStreamOptions
instância que você criou anteriormente.var rawOutgoingVideoStream = VirtualOutgoingVideoStream(videoStreamOptions: rawOutgoingVideoStreamOptions)
Implementar para o
VirtualOutgoingVideoStreamDelegate
delegado. OdidChangeFormat
evento informa sempre que oVideoStreamFormat
foi alterado a partir de um dos formatos de vídeo fornecidos na lista.virtualOutgoingVideoStream.delegate = /* Attach delegate and implement didChangeFormat */
Crie uma instância da seguinte classe auxiliar para acessar
CVPixelBuffer
dadosfinal class BufferExtensions: NSObject { public static func getArrayBuffersUnsafe(cvPixelBuffer: CVPixelBuffer) -> Array<UnsafeMutableRawPointer?> { var bufferArrayList: Array<UnsafeMutableRawPointer?> = [UnsafeMutableRawPointer?]() let cvStatus: CVReturn = CVPixelBufferLockBaseAddress(cvPixelBuffer, .readOnly) if cvStatus == kCVReturnSuccess { let bufferListSize = CVPixelBufferGetPlaneCount(cvPixelBuffer); for i in 0...bufferListSize { let bufferRef = CVPixelBufferGetBaseAddressOfPlane(cvPixelBuffer, i) bufferArrayList.append(bufferRef) } } return bufferArrayList } }
Crie uma instância da seguinte classe auxiliar para gerar 's aleatórios
RawVideoFrameBuffer
usandoVideoStreamPixelFormat.rgba
final class VideoFrameSender : NSObject { private var rawOutgoingVideoStream: RawOutgoingVideoStream private var frameIteratorThread: Thread private var stopFrameIterator: Bool = false public VideoFrameSender(rawOutgoingVideoStream: RawOutgoingVideoStream) { self.rawOutgoingVideoStream = rawOutgoingVideoStream } @objc private func VideoFrameIterator() { while !stopFrameIterator { if rawOutgoingVideoStream != nil && rawOutgoingVideoStream.format != nil && rawOutgoingVideoStream.state == .started { SendRandomVideoFrameNV12() } } } public func SendRandomVideoFrameNV12() -> Void { let videoFrameBuffer = GenerateRandomVideoFrameBuffer() rawOutgoingVideoStream.send(frame: videoFrameBuffer) { error in /*Handle error if non-nil*/ } let rate = 0.1 / rawOutgoingVideoStream.format.framesPerSecond let second: Float = 1000000 usleep(useconds_t(rate * second)) } private func GenerateRandomVideoFrameBuffer() -> RawVideoFrame { var cvPixelBuffer: CVPixelBuffer? = nil guard CVPixelBufferCreate(kCFAllocatorDefault, rawOutgoingVideoStream.format.width, rawOutgoingVideoStream.format.height, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, nil, &cvPixelBuffer) == kCVReturnSuccess else { fatalError() } GenerateRandomVideoFrameNV12(cvPixelBuffer: cvPixelBuffer!) CVPixelBufferUnlockBaseAddress(cvPixelBuffer!, .readOnly) let videoFrameBuffer = RawVideoFrameBuffer() videoFrameBuffer.buffer = cvPixelBuffer! videoFrameBuffer.streamFormat = rawOutgoingVideoStream.format return videoFrameBuffer } private func GenerateRandomVideoFrameNV12(cvPixelBuffer: CVPixelBuffer) { let w = rawOutgoingVideoStream.format.width let h = rawOutgoingVideoStream.format.height let bufferArrayList = BufferExtensions.getArrayBuffersUnsafe(cvPixelBuffer: cvPixelBuffer) guard bufferArrayList.count >= 2, let yArrayBuffer = bufferArrayList[0], let uvArrayBuffer = bufferArrayList[1] else { return } let yVal = Int32.random(in: 1..<255) let uvVal = Int32.random(in: 1..<255) for y in 0...h { for x in 0...w { yArrayBuffer.storeBytes(of: yVal, toByteOffset: Int((y * w) + x), as: Int32.self) } } for y in 0...(h/2) { for x in 0...(w/2) { uvArrayBuffer.storeBytes(of: uvVal, toByteOffset: Int((y * w) + x), as: Int32.self) } } } public func Start() { stopFrameIterator = false frameIteratorThread = Thread(target: self, selector: #selector(VideoFrameIterator), object: "VideoFrameSender") frameIteratorThread?.start() } public func Stop() { if frameIteratorThread != nil { stopFrameIterator = true frameIteratorThread?.cancel() frameIteratorThread = nil } } }
Implementar para o
VirtualOutgoingVideoStreamDelegate
. OdidChangeState
evento informa o estado do fluxo atual. Não envie quadros se o estado não for igual aVideoStreamState.started
./*Delegate Implementer*/ private var videoFrameSender: VideoFrameSender func virtualOutgoingVideoStream( _ virtualOutgoingVideoStream: VirtualOutgoingVideoStream, didChangeState args: VideoStreamStateChangedEventArgs) { switch args.stream.state { case .available: videoFrameSender = VideoFrameSender(rawOutgoingVideoStream) break case .started: /* Start sending frames */ videoFrameSender.Start() break case .stopped: /* Stop sending frames */ videoFrameSender.Stop() break } }
Vídeo ScreenShare
Como o sistema Windows gera os quadros, você deve implementar seu próprio serviço de primeiro plano para capturar os quadros e enviá-los usando a API de Chamada dos Serviços de Comunicação do Azure.
Resoluções de vídeo suportadas
Relação de aspeto | Resolução | FPS máximo |
---|---|---|
Qualquer coisa | Qualquer coisa até 1080p | 30 |
Etapas para criar um fluxo de vídeo de compartilhamento de tela
Crie uma matriz usando
VideoFormat
o VideoStreamPixelFormat suportado pelo SDK. Quando vários formatos estão disponíveis, a ordem dos formatos na lista não influencia ou prioriza qual deles é usado. Os critérios para a seleção do formato são baseados em fatores externos, como a largura de banda da rede.let videoStreamFormat = VideoStreamFormat() videoStreamFormat.width = 1280 /* Width and height can be used for ScreenShareOutgoingVideoStream for custom resolutions or use one of the predefined values inside VideoStreamResolution */ videoStreamFormat.height = 720 /*videoStreamFormat.resolution = VideoStreamResolution.p360*/ videoStreamFormat.pixelFormat = VideoStreamPixelFormat.rgba videoStreamFormat.framesPerSecond = framerate videoStreamFormat.stride1 = w * 4 /* It is times 4 because RGBA is a 32-bit format */ var videoStreamFormats: [VideoStreamFormat] = [] videoStreamFormats.append(videoStreamFormat)
Crie
RawOutgoingVideoStreamOptions
e definaVideoFormats
com o objeto criado anteriormente.var rawOutgoingVideoStreamOptions = RawOutgoingVideoStreamOptions() rawOutgoingVideoStreamOptions.formats = videoStreamFormats
Crie uma instância de
VirtualOutgoingVideoStream
usando aRawOutgoingVideoStreamOptions
instância que você criou anteriormente.var rawOutgoingVideoStream = ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions)
Capture e envie o quadro de vídeo da seguinte maneira.
private func SendRawVideoFrame() -> Void { CVPixelBuffer cvPixelBuffer = /* Fill it with the content you got from the Windows APIs, The number of buffers depends on the VideoStreamPixelFormat */ let videoFrameBuffer = RawVideoFrameBuffer() videoFrameBuffer.buffer = cvPixelBuffer! videoFrameBuffer.streamFormat = rawOutgoingVideoStream.format rawOutgoingVideoStream.send(frame: videoFrame) { error in /*Handle error if not nil*/ } }
Vídeo de entrada Raw
Este recurso lhe dá acesso aos quadros de vídeo dentro do IncomingVideoStream
, a fim de manipular esses objetos de fluxo localmente
Criar uma instância desse conjunto por meio
JoinCallOptions
daIncomingVideoOptions
configuraçãoVideoStreamKind.RawIncoming
var incomingVideoOptions = IncomingVideoOptions() incomingVideoOptions.streamType = VideoStreamKind.rawIncoming var joinCallOptions = JoinCallOptions() joinCallOptions.incomingVideoOptions = incomingVideoOptions
Depois de receber um
ParticipantsUpdatedEventArgs
evento, anexeRemoteParticipant.delegate.didChangedVideoStreamState
o delegado. Esse evento informa o estado dosIncomingVideoStream
objetos.private var remoteParticipantList: [RemoteParticipant] = [] func call(_ call: Call, didUpdateRemoteParticipant args: ParticipantsUpdatedEventArgs) { args.addedParticipants.forEach { remoteParticipant in remoteParticipant.incomingVideoStreams.forEach { incomingVideoStream in OnRawIncomingVideoStreamStateChanged(incomingVideoStream: incomingVideoStream) } remoteParticipant.delegate = /* Attach delegate OnVideoStreamStateChanged*/ } args.removedParticipants.forEach { remoteParticipant in remoteParticipant.delegate = nil } } func remoteParticipant(_ remoteParticipant: RemoteParticipant, didVideoStreamStateChanged args: VideoStreamStateChangedEventArgs) { OnRawIncomingVideoStreamStateChanged(rawIncomingVideoStream: args.stream) } func OnRawIncomingVideoStreamStateChanged(rawIncomingVideoStream: RawIncomingVideoStream) { switch incomingVideoStream.state { case .available: /* There is a new IncomingVideoStream */ rawIncomingVideoStream.delegate /* Attach delegate OnVideoFrameReceived*/ rawIncomingVideoStream.start() break; case .started: /* Will start receiving video frames */ break case .stopped: /* Will stop receiving video frames */ break case .notAvailable: /* The IncomingVideoStream should not be used anymore */ rawIncomingVideoStream.delegate = nil break } }
No momento, o
IncomingVideoStream
temVideoStreamState.available
estado anexarRawIncomingVideoStream.delegate.didReceivedRawVideoFrame
delegado, como mostrado na etapa anterior. Esse evento fornece os novosRawVideoFrame
objetos.func rawIncomingVideoStream(_ rawIncomingVideoStream: RawIncomingVideoStream, didRawVideoFrameReceived args: RawVideoFrameReceivedEventArgs) { /* Render/Modify/Save the video frame */ let videoFrame = args.frame as! RawVideoFrameBuffer }
Como desenvolvedor, você pode acessar a mídia bruta para conteúdo de entrada e saída de áudio, vídeo e compartilhamento de tela durante uma chamada, para que possa capturar, analisar e processar conteúdo de áudio/vídeo. O acesso ao áudio bruto do lado do cliente dos Serviços de Comunicação do Azure, vídeo bruto e compartilhamento de tela bruto, oferece aos desenvolvedores uma capacidade quase ilimitada de exibir e editar áudio, vídeo e conteúdo de compartilhamento de tela que acontece no SDK de Chamada dos Serviços de Comunicação do Azure. Neste início rápido, você aprenderá a implementar o acesso bruto a mídia usando o SDK de Chamada dos Serviços de Comunicação do Azure para JavaScript.
Por exemplo,
- Você pode acessar o fluxo de áudio/vídeo da chamada diretamente no objeto da chamada e enviar fluxos de áudio/vídeo de saída personalizados durante a chamada.
- Você pode inspecionar fluxos de áudio e vídeo para executar modelos de IA personalizados para análise. Esses modelos podem incluir processamento de linguagem natural para analisar conversas ou fornecer insights e sugestões em tempo real para aumentar a produtividade dos agentes.
- As organizações podem usar fluxos de mídia de áudio e vídeo para analisar o sentimento ao fornecer atendimento virtual para pacientes ou para fornecer assistência remota durante chamadas de vídeo que usam realidade mista. Esse recurso abre um caminho para que os desenvolvedores apliquem inovações para aprimorar as experiências de interação.
Pré-requisitos
Importante
Os exemplos aqui estão disponíveis na versão 1.13.1 do SDK de chamada para JavaScript. Certifique-se de usar essa versão ou mais recente quando estiver tentando este início rápido.
Aceda ao áudio raw
O acesso a mídia de áudio bruto dá acesso ao fluxo de áudio da chamada recebida, juntamente com a capacidade de visualizar e enviar fluxos de áudio de saída personalizados durante uma chamada.
Acessar um fluxo de áudio bruto de entrada
Use o código a seguir para acessar o fluxo de áudio de uma chamada recebida.
const userId = 'acs_user_id';
const call = callAgent.startCall(userId);
const callStateChangedHandler = async () => {
if (call.state === "Connected") {
const remoteAudioStream = call.remoteAudioStreams[0];
const mediaStream = await remoteAudioStream.getMediaStream();
// process the incoming call's audio media stream track
}
};
callStateChangedHandler();
call.on("stateChanged", callStateChangedHandler);
Fazer uma chamada com um fluxo de áudio personalizado
Use o código a seguir para iniciar uma chamada com um fluxo de áudio personalizado em vez de usar o dispositivo de microfone de um usuário.
const createBeepAudioStreamToSend = () => {
const context = new AudioContext();
const dest = context.createMediaStreamDestination();
const os = context.createOscillator();
os.type = 'sine';
os.frequency.value = 500;
os.connect(dest);
os.start();
const { stream } = dest;
return stream;
};
...
const userId = 'acs_user_id';
const mediaStream = createBeepAudioStreamToSend();
const localAudioStream = new LocalAudioStream(mediaStream);
const callOptions = {
audioOptions: {
localAudioStreams: [localAudioStream]
}
};
callAgent.startCall(userId, callOptions);
Mudar para um fluxo de áudio personalizado durante uma chamada
Use o código a seguir para alternar um dispositivo de entrada para um fluxo de áudio personalizado em vez de usar o dispositivo de microfone de um usuário durante uma chamada.
const createBeepAudioStreamToSend = () => {
const context = new AudioContext();
const dest = context.createMediaStreamDestination();
const os = context.createOscillator();
os.type = 'sine';
os.frequency.value = 500;
os.connect(dest);
os.start();
const { stream } = dest;
return stream;
};
...
const userId = 'acs_user_id';
const mediaStream = createBeepAudioStreamToSend();
const localAudioStream = new LocalAudioStream(mediaStream);
const call = callAgent.startCall(userId);
const callStateChangedHandler = async () => {
if (call.state === 'Connected') {
await call.startAudio(localAudioStream);
}
};
callStateChangedHandler();
call.on('stateChanged', callStateChangedHandler);
Parar um fluxo de áudio personalizado
Use o código a seguir para parar de enviar um fluxo de áudio personalizado depois que ele tiver sido definido durante uma chamada.
call.stopAudio();
Aceda a vídeo raw
A mídia de vídeo bruta fornece a instância de um MediaStream
objeto. (Para obter mais informações, consulte a documentação do JavaScript.) A mídia de vídeo bruta dá acesso específico ao objeto para MediaStream
chamadas de entrada e saída. Para vídeo bruto, você pode usar esse objeto para aplicar filtros usando aprendizado de máquina para processar quadros do vídeo.
Os quadros de vídeo de saída brutos processados podem ser enviados como um vídeo de saída do remetente. Os quadros de vídeo de entrada brutos processados podem ser renderizados no lado do recetor.
Faça uma chamada com um fluxo de vídeo personalizado
Você pode acessar o fluxo de vídeo bruto para uma chamada de saída. Você usa MediaStream
para o fluxo de vídeo bruto de saída para processar quadros usando aprendizado de máquina e para aplicar filtros. O vídeo de saída processado pode então ser enviado como um fluxo de vídeo do remetente.
Este exemplo envia dados de tela para um usuário como vídeo de saída.
const createVideoMediaStreamToSend = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 1500;
canvas.height = 845;
ctx.fillStyle = 'blue';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const colors = ['red', 'yellow', 'green'];
window.setInterval(() => {
if (ctx) {
ctx.fillStyle = colors[Math.floor(Math.random() * colors.length)];
const x = Math.floor(Math.random() * canvas.width);
const y = Math.floor(Math.random() * canvas.height);
const size = 100;
ctx.fillRect(x, y, size, size);
}
}, 1000 / 30);
return canvas.captureStream(30);
};
...
const userId = 'acs_user_id';
const mediaStream = createVideoMediaStreamToSend();
const localVideoStream = new LocalVideoStream(mediaStream);
const callOptions = {
videoOptions: {
localVideoStreams: [localVideoStream]
}
};
callAgent.startCall(userId, callOptions);
Mudar para um fluxo de vídeo personalizado durante uma chamada
Use o código a seguir para alternar um dispositivo de entrada para um fluxo de vídeo personalizado em vez de usar o dispositivo de câmera de um usuário durante uma chamada.
const createVideoMediaStreamToSend = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 1500;
canvas.height = 845;
ctx.fillStyle = 'blue';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const colors = ['red', 'yellow', 'green'];
window.setInterval(() => {
if (ctx) {
ctx.fillStyle = colors[Math.floor(Math.random() * colors.length)];
const x = Math.floor(Math.random() * canvas.width);
const y = Math.floor(Math.random() * canvas.height);
const size = 100;
ctx.fillRect(x, y, size, size);
}
}, 1000 / 30);
return canvas.captureStream(30);
};
...
const userId = 'acs_user_id';
const call = callAgent.startCall(userId);
const callStateChangedHandler = async () => {
if (call.state === 'Connected') {
const mediaStream = createVideoMediaStreamToSend();
const localVideoStream = this.call.localVideoStreams.find((stream) => { return stream.mediaStreamType === 'Video' });
await localVideoStream.setMediaStream(mediaStream);
}
};
callStateChangedHandler();
call.on('stateChanged', callStateChangedHandler);
Parar um fluxo de vídeo personalizado
Use o código a seguir para parar de enviar um fluxo de vídeo personalizado depois que ele tiver sido definido durante uma chamada.
// Stop video by passing the same `localVideoStream` instance that was used to start video
await call.stopVideo(localVideoStream);
Ao mudar de uma câmera com efeitos personalizados aplicados a outro dispositivo de câmera, primeiro pare o vídeo, alterne a fonte no LocalVideoStream
vídeo e inicie novamente.
const cameras = await this.deviceManager.getCameras();
const newCameraDeviceInfo = cameras.find(cameraDeviceInfo => { return cameraDeviceInfo.id === '<another camera that you want to switch to>' });
// If current camera is using custom raw media stream and video is on
if (this.localVideoStream.mediaStreamType === 'RawMedia' && this.state.videoOn) {
// Stop raw custom video first
await this.call.stopVideo(this.localVideoStream);
// Switch the local video stream's source to the new camera to use
this.localVideoStream?.switchSource(newCameraDeviceInfo);
// Start video with the new camera device
await this.call.startVideo(this.localVideoStream);
// Else if current camera is using normal stream from camera device and video is on
} else if (this.localVideoStream.mediaStreamType === 'Video' && this.state.videoOn) {
// You can just switch the source, no need to stop and start again. Sent video will automatically switch to the new camera to use
this.localVideoStream?.switchSource(newCameraDeviceInfo);
}
Acessar fluxo de vídeo de entrada de um participante remoto
Você pode acessar o fluxo de vídeo bruto para uma chamada de entrada. Você usa MediaStream
para o fluxo de vídeo bruto de entrada para processar quadros usando aprendizado de máquina e para aplicar filtros. O vídeo de entrada processado pode então ser renderizado no lado do recetor.
const remoteVideoStream = remoteParticipants[0].videoStreams.find((stream) => { return stream.mediaStreamType === 'Video'});
const processMediaStream = async () => {
if (remoteVideoStream.isAvailable) {
// remote video stream is turned on, process the video's raw media stream.
const mediaStream = await remoteVideoStream.getMediaStream();
} else {
// remote video stream is turned off, handle it
}
};
remoteVideoStream.on('isAvailableChanged', async () => {
await processMediaStream();
});
await processMediaStream();
Importante
Esta funcionalidade dos Serviços de Comunicação do Azure está atualmente em pré-visualização.
As APIs e SDKs de visualização são fornecidos sem um contrato de nível de serviço. Recomendamos que você não os use para cargas de trabalho de produção. Alguns recursos podem não ser suportados ou podem ter recursos restritos.
Para obter mais informações, consulte Termos de Utilização Suplementares para Pré-visualizações do Microsoft Azure.
O acesso bruto ao compartilhamento de tela está em visualização pública e disponível como parte da versão 1.15.1-beta.1+.
Aceda à partilha de ecrã em bruto
A mídia de compartilhamento de tela bruta dá acesso especificamente ao objeto para MediaStream
fluxos de compartilhamento de tela de entrada e saída. Para compartilhamento de tela bruto, você pode usar esse objeto para aplicar filtros usando aprendizado de máquina para processar quadros do compartilhamento de tela.
Os quadros de compartilhamento de tela brutos processados podem ser enviados como um compartilhamento de tela de saída do remetente. Os quadros de compartilhamento de tela de entrada processados podem ser renderizados no lado do recetor.
Nota: O envio de compartilhamento de tela só é suportado no navegador da área de trabalho.
Iniciar o compartilhamento de tela com um fluxo de compartilhamento de tela personalizado
const createVideoMediaStreamToSend = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 1500;
canvas.height = 845;
ctx.fillStyle = 'blue';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const colors = ['red', 'yellow', 'green'];
window.setInterval(() => {
if (ctx) {
ctx.fillStyle = colors[Math.floor(Math.random() * colors.length)];
const x = Math.floor(Math.random() * canvas.width);
const y = Math.floor(Math.random() * canvas.height);
const size = 100;
ctx.fillRect(x, y, size, size);
}
}, 1000 / 30);
return canvas.captureStream(30);
};
...
const mediaStream = createVideoMediaStreamToSend();
const localScreenSharingStream = new LocalVideoStream(mediaStream);
// Will start screen sharing with custom raw media stream
await call.startScreenSharing(localScreenSharingStream);
console.log(localScreenSharingStream.mediaStreamType) // 'RawMedia'
Aceda ao fluxo de partilha de ecrã bruto a partir de um ecrã, separador do browser ou aplicação e aplique efeitos ao fluxo
Segue-se um exemplo de como aplicar um efeito a preto e branco no fluxo de partilha de ecrã bruto a partir de um ecrã, separador do browser ou aplicação. NOTA: O filtro de contexto Canvas = API "grayscale(1)" não é suportado no Safari.
let bwTimeout;
let bwVideoElem;
const applyBlackAndWhiteEffect = function (stream) {
let width = 1280, height = 720;
bwVideoElem = document.createElement("video");
bwVideoElem.srcObject = stream;
bwVideoElem.height = height;
bwVideoElem.width = width;
bwVideoElem.play();
const canvas = document.createElement('canvas');
const bwCtx = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = width;
canvas.height = height;
const FPS = 30;
const processVideo = function () {
try {
let begin = Date.now();
// start processing.
// NOTE: The Canvas context filter API is not supported in Safari
bwCtx.filter = "grayscale(1)";
bwCtx.drawImage(bwVideoElem, 0, 0, width, height);
const imageData = bwCtx.getImageData(0, 0, width, height);
bwCtx.putImageData(imageData, 0, 0);
// schedule the next one.
let delay = Math.abs(1000/FPS - (Date.now() - begin));
bwTimeout = setTimeout(processVideo, delay);
} catch (err) {
console.error(err);
}
}
// schedule the first one.
bwTimeout = setTimeout(processVideo, 0);
return canvas.captureStream(FPS);
}
// Call startScreenSharing API without passing any stream parameter. Browser will prompt the user to select the screen, browser tab, or app to share in the call.
await call.startScreenSharing();
const localScreenSharingStream = call.localVideoStreams.find( (stream) => { return stream.mediaStreamType === 'ScreenSharing' });
console.log(localScreenSharingStream.mediaStreamType); // 'ScreenSharing'
// Get the raw media stream from the screen, browser tab, or application
const rawMediaStream = await localScreenSharingStream.getMediaStream();
// Apply effects to the media stream as you wish
const blackAndWhiteMediaStream = applyBlackAndWhiteEffect(rawMediaStream);
// Set the media stream with effects no the local screen sharing stream
await localScreenSharingStream.setMediaStream(blackAndWhiteMediaStream);
// Stop screen sharing and clean up the black and white video filter
await call.stopScreenSharing();
clearTimeout(bwTimeout);
bwVideoElem.srcObject.getVideoTracks().forEach((track) => { track.stop(); });
bwVideoElem.srcObject = null;
Parar de enviar fluxo de compartilhamento de tela
Use o código a seguir para parar de enviar um fluxo de compartilhamento de tela personalizado depois que ele tiver sido definido durante uma chamada.
// Stop sending raw screen sharing stream
await call.stopScreenSharing(localScreenSharingStream);
Acessar o fluxo de compartilhamento de tela de entrada de um participante remoto
Você pode acessar o fluxo de compartilhamento de tela bruto de um participante remoto. Você usa MediaStream
para o fluxo de compartilhamento de tela bruta de entrada para processar quadros usando aprendizado de máquina e para aplicar filtros. O fluxo de compartilhamento de tela de entrada processado pode então ser processado no lado do recetor.
const remoteScreenSharingStream = remoteParticipants[0].videoStreams.find((stream) => { return stream.mediaStreamType === 'ScreenSharing'});
const processMediaStream = async () => {
if (remoteScreenSharingStream.isAvailable) {
// remote screen sharing stream is turned on, process the stream's raw media stream.
const mediaStream = await remoteScreenSharingStream.getMediaStream();
} else {
// remote video stream is turned off, handle it
}
};
remoteScreenSharingStream.on('isAvailableChanged', async () => {
await processMediaStream();
});
await processMediaStream();
Próximos passos
- Verifique a condição da rede com a ferramenta de diagnóstico
- Explore APIs de diagnóstico voltadas para o usuário
- Habilitar estatísticas de qualidade de mídia em seu aplicativo
- Consumir logs de chamadas com o Azure Monitor
Artigos relacionados
- Confira a amostra do herói chamador.
- Comece a usar a Biblioteca da Interface do Usuário.
- Saiba mais sobre os recursos do SDK de chamada.
- Saiba mais sobre como funciona a chamada.