Início Rápido: adicione acesso à mídia bruta ao seu aplicativo
Neste 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 enviarem ou renderizarem quadros de vídeo brutos dos participantes remotos em uma chamada. Este início rápido se baseia no Início Rápido: adicione chamada de vídeo individual ao seu aplicativo para Unity.
Acesso ao RawVideo
Devido ao fato de que o aplicativo irá gerar os quadros de vídeo, o próprio aplicativo precisa informar ao SDK de Chamada dos Serviços de Comunicação do Azure quais formatos de vídeo o aplicativo é capaz de 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 da rede naquele momento.
Vídeo Virtual
Resoluções de vídeo com suporte
Taxa de proporção | 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 Início Rápido: adicione chamada de vídeo individual ao seu aplicativo para criar um jogo do Unity. O objetivo é obter um objeto
CallAgent
pronto para iniciar a chamada. Encontre o código finalizado para este guia de início rápido no GitHub.Crie uma matriz de
VideoFormat
usando o VideoStreamPixelFormat compatível com o SDK. Quando vários formatos estiverem disponíveis, a ordem do formato na lista não irá influenciar nem priorizar qual deles será usado. Os critérios para seleção de formato são baseados em fatores externos, como largura de banda de 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 os objetos criados anteriormente.var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions { Formats = videoStreamFormats };
Crie uma instância de
VirtualOutgoingVideoStream
usando asRawOutgoingVideoStreamOptions
da instância que você criou anteriormente.var rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
Faça uma assinatura do delegado do
RawOutgoingVideoStream.FormatChanged
. Esse evento informa sempre que oVideoStreamFormat
foi alterado de um dos formatos de vídeo fornecidos na lista.rawOutgoingVideoStream.FormatChanged += (object sender, VideoStreamFormatChangedEventArgs args) { VideoStreamFormat videoStreamFormat = args.Format; }
Faça uma assinatura do delegado do
RawOutgoingVideoStream.StateChanged
. Esse evento informa sempre que oState
foi alterado.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; } }
Lidar com transações brutas de estado de fluxo de vídeo de saída, como Iniciar e Parar, e começar a gerar quadros de vídeo personalizados ou suspender 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 de 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 }; }
Observação
unsafe
modificador é usado nesse método, poisNativeBuffer
requer acesso aos recursos de memória nativos. Portanto, a opçãoAllow unsafe
também precisa ser habilitada no Editor do Unity.Da mesma forma, podemos lidar com quadros de vídeo de entrada em resposta ao evento do fluxo de vídeo
StateChanged
.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 }); }
Recomenda-se enfaticamente gerenciar os quadros de vídeo de entrada e saída por um mecanismo de buffer para evitar a sobrecarga do método de retorno de chamada
MonoBehaviour.Update()
, que deve ser mantido com pouca carga e evitar tarefas pesadas de CPU ou de rede, além de garantir uma experiência de vídeo sem interrupções. Essa otimização opcional é deixada para os desenvolvedores decidirem o que funciona melhor nos seus cenários.Aqui está um exemplo de como os quadros de entrada podem ser renderizados em um
VideoTexture
do Unity chamandoGraphics.Blit
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 à mídia bruta 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 no guia Início Rápido: adicione chamadas de vídeo individuais ao seu aplicativo para Windows.
Acesso RawAudio
O acesso à mídia de áudio bruto lhe dá acesso ao fluxo de áudio de uma chamada sendo recebida, além da capacidade de ver e enviar fluxos de áudio personalizados durante uma chamada.
Enviar Áudio de Saída Bruto
Crie um objeto de opções especificando as propriedades brutas do fluxo que desejamos 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 às opções de ingresso na chamada e o fluxo será iniciado automaticamente quando a chamada estiver 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 Call
existente:
await call.StartAudio(rawOutgoingAudioStream);
Começar 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 evento OnStateChangedListener
.
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 for iniciado, podemos começar a enviar MemoryBuffer
amostras de áudio para a chamada.
O formato de 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 Bruto de entrada
Também podemos receber os exemplos de fluxo de áudio de chamada como MemoryBuffer
se quisermos processar o fluxo de áudio de chamada antes da reprodução.
Criar um objeto RawIncomingAudioStreamOptions
especificando as propriedades brutas do fluxo que desejamos enviar.
RawIncomingAudioStreamProperties properties = new RawIncomingAudioStreamProperties()
{
Format = AudioStreamFormat.Pcm16Bit,
SampleRate = AudioStreamSampleRate.Hz44100,
ChannelMode = AudioStreamChannelMode.Stereo
};
RawIncomingAudioStreamOptions options = new RawIncomingAudioStreamOptions()
{
Properties = properties
};
Criar um RawIncomingAudioStream
e o anexe às opções de ingresso na chamada
JoinCallOptions options = JoinCallOptions(); // or StartCallOptions()
RawIncomingAudioStream rawIncomingAudioStream = new RawIncomingAudioStream(audioStreamOptions);
IncomingAudioOptions incomingAudioOptions = new IncomingAudioOptions()
{
Stream = rawIncomingAudioStream
};
options.IncomingAudioOptions = incomingAudioOptions;
Ou você também pode anexar o fluxo a uma instância Call
existente:
await call.startAudio(context, rawIncomingAudioStream);
Para começar a receber buffers de áudio brutos do fluxo de entrada, adicione ouvintes ao estado de 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
Devido ao fato de que o aplicativo irá gerar os quadros de vídeo, o próprio aplicativo precisa informar ao SDK de Chamada dos Serviços de Comunicação do Azure quais formatos de vídeo o aplicativo é capaz de 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 da rede naquele momento.
Vídeo Virtual
Resoluções de vídeo com suporte
Taxa de proporção | 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 de
VideoFormat
usando o VideoStreamPixelFormat compatível com o SDK. Quando vários formatos estiverem disponíveis, a ordem do formato na lista não irá influenciar nem priorizar qual deles será usado. Os critérios para seleção de formato são baseados em fatores externos, como largura de banda de 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 os objetos criados anteriormente.var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions { Formats = videoStreamFormats };
Crie uma instância de
VirtualOutgoingVideoStream
usando asRawOutgoingVideoStreamOptions
da instância que você criou anteriormente.var rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
Faça uma assinatura do delegado do
RawOutgoingVideoStream.FormatChanged
. Esse evento informa sempre que oVideoStreamFormat
foi alterado 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 classe auxiliar a seguir 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 classe auxiliar a seguir para gerar
RawVideoFrame
aleatórios por meio deVideoStreamPixelFormat.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; } } }
Faça uma assinatura do delegado do
VideoStream.StateChanged
. 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
Devido ao fato de que o sistema Windows gera os quadros, você deve implementar seu próprio serviço em primeiro plano para capturar os quadros e enviá-los usando nossa API de Chamada de Serviços de Comunicação do Azure.
Resoluções de vídeo com suporte
Taxa de proporção | Resolução | FPS máximo |
---|---|---|
Nada | Qualquer configuração até 1080p | 30 |
Etapas para criar um fluxo de vídeo de compartilhamento de tela
- Crie uma matriz de
VideoFormat
usando o VideoStreamPixelFormat compatível com o SDK. Quando vários formatos estiverem disponíveis, a ordem do formato na lista não irá influenciar nem priorizar qual deles será usado. Os critérios para seleção de formato são baseados em fatores externos, como largura de banda de 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 os objetos criados anteriormente.var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions { Formats = videoStreamFormats };
- Crie uma instância de
VirtualOutgoingVideoStream
usando asRawOutgoingVideoStreamOptions
da 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 Bruto
Esse recurso fornece acesso aos quadros de vídeo dentro dos IncomingVideoStream
para manipular esses fluxos localmente
- Crie uma instância de
IncomingVideoOptions
que define por meio daJoinCallOptions
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 delegado de
ParticipantsUpdatedEventArgs
anexação de eventoRemoteParticipant.VideoStreamStateChanged
. Esse evento informa o estado dos objetosIncomingVideoStream
.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
tem o delegado de anexaçãoVideoStreamState.Available
de estadoRawIncomingVideoStream.RawVideoFrameReceived
, conforme mostrado na etapa anterior. Isso fornece os novos objetosRawVideoFrame
.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 guia de início rápido, você aprenderá a implementar o acesso à mídia bruta por meio do 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 se baseia no guia Início Rápido: adicione chamadas de vídeo individuais ao seu aplicativo para Android.
Acesso ao RawAudio
O acesso à mídia de áudio bruto lhe dá acesso ao fluxo de áudio de uma chamada sendo recebida, além da capacidade de ver e enviar fluxos de áudio personalizados durante uma chamada.
Enviar Áudio de Saída Bruto
Crie um objeto de opções especificando as propriedades brutas do fluxo que desejamos 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 às opções de ingresso na chamada e o fluxo será iniciado automaticamente quando a chamada estiver 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 Call
existente:
CompletableFuture<Void> result = call.startAudio(context, rawOutgoingAudioStream);
Começar 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 evento OnStateChangedListener
.
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 for iniciado, podemos começar a enviar java.nio.ByteBuffer
amostras de áudio para a chamada.
O formato de 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 Bruto de entrada
Também podemos receber os exemplos de fluxo de áudio de chamada como java.nio.ByteBuffer
se quisermos processar o fluxo de áudio de chamada antes da reprodução.
Crie um objeto RawIncomingAudioStreamOptions
especificando as propriedades brutas do fluxo que desejamos enviar.
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 o anexe às opções de ingresso na chamada
JoinCallOptions options = JoinCallOptions() // or StartCallOptions()
IncomingAudioOptions incomingAudioOptions = new IncomingAudioOptions();
RawIncomingAudioStream rawIncomingAudioStream = new RawIncomingAudioStream(audioStreamOptions);
incomingAudioOptions.setStream(rawIncomingAudioStream);
options.setIncomingAudioOptions(incomingAudioOptions);
Ou você também pode anexar o fluxo a uma instância Call
existente:
CompletableFuture<Void> result = call.startAudio(context, rawIncomingAudioStream);
Para começar a receber buffers de áudio brutos do fluxo de entrada, adicione ouvintes ao estado de 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 parar o fluxo de áudio na instância de chamada Call
atual:
CompletableFuture<Void> result = call.stopAudio(context, rawIncomingAudioStream);
Acesso ao RawVideo
Devido ao fato de que o aplicativo irá gerar os quadros de vídeo, o próprio aplicativo precisa informar ao SDK de Chamada dos Serviços de Comunicação do Azure quais formatos de vídeo o aplicativo é capaz de 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 da rede naquele momento.
Vídeo Virtual
Resoluções de vídeo com suporte
Taxa de proporção | 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 de
VideoFormat
usando o VideoStreamPixelFormat compatível com o SDK.Quando vários formatos estiverem disponíveis, a ordem do formato na lista não irá influenciar nem priorizar qual deles será usado. Os critérios para seleção de formato são baseados em fatores externos, como largura de banda de 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 os objetos criados anteriormente.RawOutgoingVideoStreamOptions rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions(); rawOutgoingVideoStreamOptions.setFormats(videoStreamFormats);
Crie uma instância de
VirtualOutgoingVideoStream
usando asRawOutgoingVideoStreamOptions
da instância que você criou anteriormente.VirtualOutgoingVideoStream rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
Faça uma assinatura do delegado do
RawOutgoingVideoStream.addOnFormatChangedListener
. Esse evento informa sempre que oVideoStreamFormat
foi alterado de um dos formatos de vídeo fornecidos na lista.virtualOutgoingVideoStream.addOnFormatChangedListener((VideoStreamFormatChangedEvent args) -> { VideoStreamFormat videoStreamFormat = args.Format; });
Crie uma instância da classe auxiliar a seguir para gerar
RawVideoFrame
aleatórios por meio deVideoStreamPixelFormat.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(); } } }
Faça uma assinatura do delegado do
VideoStream.addOnStateChangedListener
. Esse delegado informará o estado do fluxo em curso. 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 do ScreenShare
Devido ao fato de que o sistema Windows gera os quadros, você deve implementar seu próprio serviço em primeiro plano para capturar os quadros e enviá-los usando nossa API de Chamada de Serviços de Comunicação do Azure.
Resoluções de vídeo com suporte
Taxa de proporção | Resolução | FPS máximo |
---|---|---|
Nada | Qualquer configuração até 1080p | 30 |
Etapas para criar um fluxo de vídeo de compartilhamento de tela
Crie uma matriz de
VideoFormat
usando o VideoStreamPixelFormat compatível com o SDK.Quando vários formatos estiverem disponíveis, a ordem do formato na lista não irá influenciar nem priorizar qual deles será usado. Os critérios para seleção de formato são baseados em fatores externos, como largura de banda de 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 os objetos criados anteriormente.RawOutgoingVideoStreamOptions rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions(); rawOutgoingVideoStreamOptions.setFormats(videoStreamFormats);
Crie uma instância de
VirtualOutgoingVideoStream
usando asRawOutgoingVideoStreamOptions
da 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 Bruto
Esse recurso fornece acesso aos quadros de vídeo dentro dos objetos IncomingVideoStream
para manipular esses fluxos localmente
Crie uma instância de
IncomingVideoOptions
que define por meio daJoinCallOptions
configuraçãoVideoStreamKind.RawIncoming
IncomingVideoOptions incomingVideoOptions = new IncomingVideoOptions() .setStreamType(VideoStreamKind.RAW_INCOMING); JoinCallOptions joinCallOptions = new JoinCallOptions() .setIncomingVideoOptions(incomingVideoOptions);
Depois de receber um delegado de anexação
ParticipantsUpdatedEventArgs
de eventoRemoteParticipant.VideoStreamStateChanged
. Esse evento informa o estado do objetoIncomingVideoStream
.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
tem o delegado de anexaçãoVideoStreamState.Available
de estadoRawIncomingVideoStream.RawVideoFrameReceived
, conforme mostrado na etapa anterior. Esse delegado fornece os novos objetosRawVideoFrame
.private void OnVideoFrameReceived(RawVideoFrameReceivedEventArgs args) { // Render/Modify/Save the video frame RawVideoFrameBuffer videoFrame = (RawVideoFrameBuffer) args.getFrame(); }
Neste guia de início rápido, você aprenderá a implementar o acesso à mídia bruta por meio do 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 se baseia no guia Início Rápido: adicione chamadas de vídeo individuais ao seu aplicativo para iOS.
Acesso ao RawAudio
O acesso à mídia de áudio bruto lhe dá acesso ao fluxo de áudio de uma chamada sendo recebida, além da capacidade de ver e enviar fluxos de áudio personalizados durante uma chamada.
Enviar Áudio de Saída Bruto
Crie um objeto de opções especificando as propriedades brutas do fluxo que desejamos 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 às opções de ingresso na chamada e o fluxo será iniciado automaticamente quando a chamada estiver 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 Call
existente:
call.startAudio(stream: self.rawOutgoingAudioStream) { error in
// Stream attached to `Call`.
}
Começar 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, implementamos o RawOutgoingAudioStreamDelegate
. E defina-o 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 use o fechamento com base em
self.rawOutgoingAudioStream.events.onStateChanged = { args in
// When value is `AudioStreamState.started` we will be able to send audio samples.
}
Quando o fluxo for iniciado, podemos começar a enviar AVAudioPCMBuffer
amostras de áudio para a chamada.
O formato de 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 parar o fluxo de áudio na instância de chamada Call
atual:
call.stopAudio(stream: self.rawOutgoingAudioStream) { error in
// Stream detached from `Call` and stopped.
}
Capturando exemplos do microfone
Por meio do AVAudioEngine
da Apple, podemos capturar quadros de microfone tocando no nó de entrada do mecanismo de áudio. Também podemos processar o áudio antes de enviá-los para uma chamada capturando os dados do microfone e por meio da funcionalidade de áudio bruto.
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()
}
}
Observação
A taxa de exemplo do nó de entrada do mecanismo de áudio usa como padrão um valor > da taxa de exemplo preferencial para a sessão de áudio compartilhado. Portanto, não é possível instalar o toque nesse nó por meio de um valor diferente.
Portanto, precisamos garantir que a taxa de exemplo de propriedades RawOutgoingStream
corresponda à que obtemos do toque em exemplos de microfone ou converter os buffers de toque no formato que corresponde ao esperado no fluxo de saída.
Com este pequeno exemplo, aprendemos como podemos capturar os dados do microfone AVAudioEngine
e enviar esses exemplos para uma chamada usando o recurso de áudio de saída bruto.
Receber Áudio Bruto de entrada
Também podemos receber os exemplos de fluxo de áudio de chamada como AVAudioPCMBuffer
se quisermos processar o fluxo de áudio de chamada antes da reprodução.
Crie um objeto RawIncomingAudioStreamOptions
especificando as propriedades brutas do fluxo que desejamos enviar.
let options = RawIncomingAudioStreamOptions()
let properties = RawIncomingAudioStreamProperties()
properties.format = .pcm16Bit
properties.sampleRate = .hz44100
properties.channelMode = .stereo
options.properties = properties
Criar um RawOutgoingAudioStream
e o anexe às opções de ingresso na chamada
let options = JoinCallOptions() // or StartCallOptions()
let incomingAudioOptions = IncomingAudioOptions()
self.rawIncomingStream = RawIncomingAudioStream(rawIncomingAudioStreamOptions: audioStreamOptions)
incomingAudioOptions.stream = self.rawIncomingStream
options.incomingAudioOptions = incomingAudioOptions
Ou você também pode anexar o fluxo a uma instância Call
existente:
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
Devido ao fato de que o aplicativo irá gerar os quadros de vídeo, o próprio aplicativo precisa informar ao SDK de Chamada dos Serviços de Comunicação do Azure quais formatos de vídeo o aplicativo é capaz de 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 da rede naquele momento.
Vídeo Virtual
Resoluções de vídeo com suporte
Taxa de proporção | 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 de
VideoFormat
usando o VideoStreamPixelFormat compatível com o SDK. Quando vários formatos estiverem disponíveis, a ordem do formato na lista não irá influenciar nem priorizar qual deles será usado. Os critérios para seleção de formato são baseados em fatores externos, como largura de banda de 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 os formatos com o objetos criado anteriormente.var rawOutgoingVideoStreamOptions = RawOutgoingVideoStreamOptions() rawOutgoingVideoStreamOptions.formats = videoStreamFormats
Crie uma instância de
VirtualOutgoingVideoStream
usando asRawOutgoingVideoStreamOptions
da instância que você criou anteriormente.var rawOutgoingVideoStream = VirtualOutgoingVideoStream(videoStreamOptions: rawOutgoingVideoStreamOptions)
Implemente ao delegado
VirtualOutgoingVideoStreamDelegate
. O eventodidChangeFormat
informa sempre que oVideoStreamFormat
foi alterado de um dos formatos de vídeo fornecidos na lista.virtualOutgoingVideoStream.delegate = /* Attach delegate and implement didChangeFormat */
Crie uma instância da classe auxiliar a seguir para acessar os dados do
CVPixelBuffer
final 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 classe auxiliar a seguir para gerar
RawVideoFrameBuffer
aleatórios por meio deVideoStreamPixelFormat.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 } } }
Implemente para o
VirtualOutgoingVideoStreamDelegate
. O eventodidChangeState
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 do ScreenShare
Devido ao fato de que o sistema Windows gera os quadros, você deve implementar seu próprio serviço em primeiro plano para capturar os quadros e enviá-los usando nossa API de Chamada de Serviços de Comunicação do Azure.
Resoluções de vídeo com suporte
Taxa de proporção | Resolução | FPS máximo |
---|---|---|
Nada | Qualquer configuração até 1080p | 30 |
Etapas para criar um fluxo de vídeo de compartilhamento de tela
Crie uma matriz de
VideoFormat
usando o VideoStreamPixelFormat compatível com o SDK. Quando vários formatos estiverem disponíveis, a ordem do formato na lista não irá influenciar nem priorizar qual deles será usado. Os critérios para seleção de formato são baseados em fatores externos, como largura de banda de 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 os objetos criados anteriormente.var rawOutgoingVideoStreamOptions = RawOutgoingVideoStreamOptions() rawOutgoingVideoStreamOptions.formats = videoStreamFormats
Crie uma instância de
VirtualOutgoingVideoStream
usando asRawOutgoingVideoStreamOptions
da 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 Bruto
Esse recurso fornece acesso aos quadros de vídeo dentro dos IncomingVideoStream
para manipular esses fluxos localmente
Crie uma instância de
IncomingVideoOptions
que define por meio daJoinCallOptions
configuraçãoVideoStreamKind.RawIncoming
var incomingVideoOptions = IncomingVideoOptions() incomingVideoOptions.streamType = VideoStreamKind.rawIncoming var joinCallOptions = JoinCallOptions() joinCallOptions.incomingVideoOptions = incomingVideoOptions
Depois de receber um delegado de
ParticipantsUpdatedEventArgs
anexação de eventoRemoteParticipant.delegate.didChangedVideoStreamState
. Esse evento informa o estado dos objetosIncomingVideoStream
.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
tem o delegado deVideoStreamState.available
anexação de estadoRawIncomingVideoStream.delegate.didReceivedRawVideoFrame
, conforme mostrado na etapa anterior. Esse evento fornece os novos objetosRawVideoFrame
.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údos de áudio, vídeo e compartilhamento de tela de entrada e saída durante uma chamada, para que possa capturar, analisar e processar o conteúdo de áudio/vídeo. O acesso ao áudio bruto, ao vídeo bruto e ao compartilhamento de tela bruto do lado do cliente dos Serviços de Comunicação do Azure oferece aos desenvolvedores uma capacidade quase ilimitada de exibir e editar o conteúdo de áudio, vídeo e compartilhamento de tela que ocorre no SDK de Chamada dos Serviços de Comunicação do Azure. Neste guia de início rápido, você aprenderá a implementar o acesso à mídia bruta 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 personalizados de áudio/vídeo de saída durante a chamada.
- Você pode inspecionar os fluxos de áudio e vídeo para executar modelos personalizados de IA 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 do agente.
- As organizações podem usar fluxos de mídia de áudio e vídeo para analisar sentimentos ao fornecer um atendimento virtual a pacientes ou fornecer ajuda remota durante chamadas de vídeo que usam realidade mista. Esse recurso abre caminho para que os desenvolvedores apliquem inovações e aprimorem as experiências de interação.
Pré-requisitos
Importante
Os exemplos daqui estão disponível na versão 1.13.1 do SDK de chamada para JavaScript. Certifique-se de usar essa versão ou uma mais recente ao experimentar este início rápido.
Acesso ao áudio bruto
O acesso à mídia de áudio bruto lhe dá acesso ao fluxo de áudio de uma chamada sendo recebida, além da capacidade de ver e enviar fluxos de áudio personalizados durante uma chamada.
Acesso a um fluxo de áudio bruto sendo recebido
Use o código a seguir para acessar o fluxo de áudio de uma chamada sendo 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);
Como 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);
Como alternar para um fluxo de áudio personalizado durante uma chamada
Use o código a seguir para alternar o dispositivo de entrada para um fluxo de áudio personalizado em vez de usar o dispositivo de microfone do 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);
Como interromper um fluxo de áudio personalizado
Use o código a seguir para parar de enviar um fluxo de áudio personalizado após ele ter sido configurado durante uma chamada.
call.stopAudio();
Acesso ao vídeo bruto
A mídia de vídeo bruta lhe fornece a instância de um objeto de MediaStream
. (Para obter mais informações, consulte a documentação do JavaScript.) A mídia de vídeo bruta fornece acesso especificamente ao objeto de MediaStream
para chamadas sendo recebidas e efetuadas. No caso de um vídeo bruto, você pode usar esse objeto para aplicar filtros usando o aprendizado de máquina para processar quadros do vídeo.
Os quadros de vídeo brutos de saída processados podem ser enviados como um vídeo de saída do remetente. Os quadros de vídeo bruto processados sendo recebidos podem ser renderizados no lado do destinatário.
Como fazer uma chamada com um fluxo de vídeo personalizado
Você pode acessar o fluxo de vídeo bruto para uma chamada sendo efetuada. Você usa MediaStream
para o fluxo de vídeo bruto de saída para processar quadros usando o aprendizado de máquina e aplicar filtros. O vídeo de saída processado pode, a seguir, ser enviado como um fluxo de vídeo do remetente.
Esse exemplo envia dados do painel da tela para um usuário na forma de um 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);
Como alternar para um fluxo de vídeo personalizado durante uma chamada
Use o código a seguir para alternar o dispositivo de entrada para um fluxo de vídeo personalizado em vez de usar o dispositivo de câmera do 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);
Como interromper um fluxo de vídeo personalizado
Use o código a seguir para parar de enviar um fluxo de vídeo personalizado após ele ter sido configurado durante uma chamada.
// Stop video by passing the same `localVideoStream` instance that was used to start video
await call.stopVideo(localVideoStream);
Ao alternar de uma câmera com efeitos personalizados aplicados para outro dispositivo de câmera, primeiro pare o vídeo, alterne a fonte no LocalVideoStream
e inicie o vídeo 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);
}
Acesse o fluxo de vídeo de entrada de um participante remoto
Você pode acessar o fluxo de vídeo bruto para uma chamada sendo recebida. Você usa MediaStream
para o fluxo de vídeo bruto sendo recebido para processar quadros usando o aprendizado de máquina e aplicar filtros. O vídeo de entrada processado sendo recebido pode ser renderizado no lado do destinatário.
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
Este recurso dos Serviços de Comunicação do Azure estão atualmente em versão prévia.
Versões prévias das APIs e dos SDKs são fornecidas sem um contrato de nível de serviço. É recomendável que você não as use para cargas de trabalho de produção. Alguns recursos podem não ter suporte ou talvez ter restrição de recursos.
Para saber mais, consulte os Termos de Uso Complementares das Versões Prévias do Microsoft Azure.
O acesso de compartilhamento de tela bruto está em visualização pública e disponível como parte da versão 1.15.1-beta.1+.
Acessar o compartilhamento de tela bruta
A mídia de compartilhamento de tela bruta dá acesso especificamente ao objeto MediaStream
para fluxos de compartilhamento de tela de entrada e saída. Para o compartilhamento de tela bruta, você pode usar esse objeto para aplicar filtros usando o aprendizado de máquina para processar os 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 brutos processados podem ser renderizados no lado do receptor.
Observação: o envio de compartilhamento de tela só tem suporte 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'
Acesse o fluxo de compartilhamento de tela bruto a partir de uma tela, guia do navegador ou aplicativo e aplique efeitos ao fluxo
A seguir, um exemplo de como aplicar um efeito preto e branco no fluxo de compartilhamento de tela bruto de uma tela, guia do navegador ou aplicativo. OBSERVAÇÃO: o filtro de contexto do Canvas = API "grayscale(1)" não tem suporte 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 interromper o envio de 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 recebido de um participante remoto
Você pode acessar o fluxo de compartilhamento de tela bruto de um participante remoto. Use MediaStream
para o fluxo de compartilhamento de tela bruto de entrada para processar quadros usando o aprendizado de máquina e para aplicar filtros. O fluxo de compartilhamento de tela de entrada processado pode então ser renderizado no lado do receptor.
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óximas etapas
- Verificar sua condição de rede com a ferramenta de diagnóstico
- Explorar as APIs de diagnóstico voltadas ao usuário
- Habilitar as estatísticas de qualidade de mídia no aplicativo
- Consumir logs de chamadas com o Azure Monitor
Artigos relacionados
- Confira nosso exemplo emblemático de chamada.
- Comece a usar a Biblioteca de Interfaces do Usuário.
- Saiba mais sobre os recursos do SDK de Chamada.
- Saiba mais sobre como a chamada funciona.