Inicio rápido: Adición de acceso a elementos multimedia sin procesar a la aplicación
En este inicio rápido, aprenderá a implementar el acceso a elementos multimedia sin procesar con el SDK de llamadas de Azure Communication Services para Unity. El SDK de llamadas de Azure Communication Services ofrece API que permiten a las aplicaciones generar sus propios fotogramas de vídeo para enviar o representar fotogramas de vídeo sin procesar de participantes remotos en una llamada. Este inicio rápido se basa en el Inicio rápido: Adición de llamadas de vídeo 1:1 a la aplicación para Unity.
Acceso a vídeos sin procesar
Puesto que la aplicación genera los fotogramas de vídeo, esta debe informar al SDK de llamadas de Azure Communication Services sobre los formatos de vídeo que la aplicación es capaz de generar. Esta información permite que el SDK de llamadas de Azure Communication Services elija la mejor configuración de formato de vídeo dadas las condiciones de red en un momento determinado.
Vídeo virtual
Resoluciones de vídeo admitidas
Relación de aspecto | Solución | 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 los pasos que se indican aquí Inicio rápido: Adición de llamadas de vídeo 1:1 a la aplicación para crear un juego de Unity. El objetivo es obtener un objeto
CallAgent
listo para iniciar la llamada. Busque el código finalizado de este inicio rápido en GitHub.Cree una matriz de
VideoFormat
con el VideoStreamPixelFormat compatible con el SDK. Cuando hay varios formatos disponibles, el orden de los formatos en la lista no influye ni da prioridad a cuál se utiliza. Los criterios de selección de formato se basan en factores externos, como el ancho de banda de red.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 };
Cree
RawOutgoingVideoStreamOptions
y configureFormats
con el objeto creado anteriormente.var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions { Formats = videoStreamFormats };
Cree una instancia de
VirtualOutgoingVideoStream
mediante la instanciaRawOutgoingVideoStreamOptions
que creó anteriormente.var rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
Suscríbase al
RawOutgoingVideoStream.FormatChanged
delegado. Este evento informa siempre que se cambieVideoStreamFormat
de uno de los formatos de vídeo proporcionados en la lista.rawOutgoingVideoStream.FormatChanged += (object sender, VideoStreamFormatChangedEventArgs args) { VideoStreamFormat videoStreamFormat = args.Format; }
Suscríbase al
RawOutgoingVideoStream.StateChanged
delegado. Este evento informa cada vez queState
ha cambiado.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; } }
Controle transacciones de estado de secuencia de vídeo salientes sin procesar, como Start y Stop, y comience a generar fotogramas de vídeo personalizados o suspender el algoritmo de generación de fotogramas.
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; } }
Este es un ejemplo del generador de fotogramas de vídeo salientes:
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:
El modificador
unsafe
se usa en este método, ya queNativeBuffer
requiere acceso a los recursos de memoria nativos. Por lo tanto, la opciónAllow unsafe
también debe habilitarse en el Editor de Unity.Del mismo modo, podemos controlar los fotogramas de vídeo entrantes en respuesta al evento
StateChanged
de secuencia 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 }); }
Se recomienda administrar fotogramas de vídeo entrantes y salientes a través de un mecanismo de almacenamiento en búfer para evitar sobrecargar el método de devolución de llamada
MonoBehaviour.Update()
, que debe mantenerse ligero y evitar tareas pesadas de CPU o red y garantizar una experiencia de vídeo más fluida. Esta optimización opcional se deja a los desarrolladores para decidir cuál funciona mejor en sus escenarios.Este es un ejemplo de cómo se pueden representar los fotogramas entrantes en Unity
VideoTexture
mediante una llamadaGraphics.Blit
fuera de una cola 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(); } }
En este inicio rápido, aprenderá a implementar el acceso a elementos multimedia sin procesar mediante el SDK de llamada de Azure Communication Services para Windows. El SDK de llamadas de Azure Communication Services ofrece API que permiten a las aplicaciones generar sus propios fotogramas de vídeo para enviarlos a los participantes remotos en una llamada. Este inicio rápido se basa en el Inicio rápido: Adición de llamadas de vídeo 1:1 a la aplicación para Windows.
Acceso a audio sin procesar
El acceso a medios de audio sin procesar proporciona acceso a la secuencia de audio de la llamada entrante, junto con la capacidad de ver y enviar secuencias de audio salientes personalizadas durante una llamada.
Enviar audio saliente sin procesar
Crear un objeto de opciones que especifique las propiedades de secuencia sin procesar 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
};
Cree una instancia de RawOutgoingAudioStream
y adjúntela para unir las opciones de llamada y la secuencia se iniciará automáticamente cuando se conecte la llamada.
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.
Adjuntar secuencia a una llamada
También puede adjuntar la secuencia a una instancia existente de Call
en su lugar:
await call.StartAudio(rawOutgoingAudioStream);
Empezar a enviar ejemplos sin procesar
Solo podemos empezar a enviar datos una vez que el estado de la secuencia sea AudioStreamState.Started
.
Para observar el cambio de estado de la secuencia de audio, agregue un agente de escucha al evento de OnStateChangedListener
.
unsafe private void AudioStateChanged(object sender, AudioStreamStateChanged args)
{
if (args.AudioStreamState == AudioStreamState.Started)
{
// We can now start sending samples.
}
}
outgoingAudioStream.StateChanged += AudioStateChanged;
Cuando se inicia la secuencia, podemos empezar a enviar ejemplos de audio de MemoryBuffer
a la llamada.
El formato del búfer de audio debe coincidir con las propiedades de la secuencia 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();
}
Recibir audio entrante sin procesar
También podemos recibir los ejemplos de secuencias de audio de llamada como MemoryBuffer
si queremos procesar la secuencia de audio de llamada antes de la reproducción.
Crear un objeto de RawIncomingAudioStreamOptions
que especifique las propiedades de secuencia sin procesar que queremos recibir.
RawIncomingAudioStreamProperties properties = new RawIncomingAudioStreamProperties()
{
Format = AudioStreamFormat.Pcm16Bit,
SampleRate = AudioStreamSampleRate.Hz44100,
ChannelMode = AudioStreamChannelMode.Stereo
};
RawIncomingAudioStreamOptions options = new RawIncomingAudioStreamOptions()
{
Properties = properties
};
Cree una instancia de RawIncomingAudioStream
y adjúntela para unir las opciones de llamada
JoinCallOptions options = JoinCallOptions(); // or StartCallOptions()
RawIncomingAudioStream rawIncomingAudioStream = new RawIncomingAudioStream(audioStreamOptions);
IncomingAudioOptions incomingAudioOptions = new IncomingAudioOptions()
{
Stream = rawIncomingAudioStream
};
options.IncomingAudioOptions = incomingAudioOptions;
También podemos adjuntar la secuencia a una instancia existente de Call
en su lugar:
await call.startAudio(context, rawIncomingAudioStream);
Para empezar a recibir búferes de audio sin procesar de la secuencia entrante, agregue agentes de escucha al estado de la secuencia entrante y a los eventos recibidos del búfer.
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;
Acceso a vídeos sin procesar
Puesto que la aplicación genera los fotogramas de vídeo, esta debe informar al SDK de llamadas de Azure Communication Services sobre los formatos de vídeo que la aplicación es capaz de generar. Esta información permite que el SDK de llamadas de Azure Communication Services elija la mejor configuración de formato de vídeo dadas las condiciones de red en un momento determinado.
Vídeo virtual
Resoluciones de vídeo admitidas
Relación de aspecto | Solución | 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 |
Cree una matriz de
VideoFormat
con el VideoStreamPixelFormat compatible con el SDK. Cuando hay varios formatos disponibles, el orden de los formatos en la lista no influye ni da prioridad a cuál se utiliza. Los criterios de selección de formato se basan en factores externos, como el ancho de banda de red.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 };
Cree
RawOutgoingVideoStreamOptions
y configureFormats
con el objeto creado anteriormente.var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions { Formats = videoStreamFormats };
Cree una instancia de
VirtualOutgoingVideoStream
mediante la instanciaRawOutgoingVideoStreamOptions
que creó anteriormente.var rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
Suscríbase al
RawOutgoingVideoStream.FormatChanged
delegado. Este evento informa siempre que se cambieVideoStreamFormat
de uno de los formatos de vídeo proporcionados en la lista.rawOutgoingVideoStream.FormatChanged += (object sender, VideoStreamFormatChangedEventArgs args) { VideoStreamFormat videoStreamFormat = args.Format; }
Cree una instancia de la siguiente clase auxiliar para acceder a los datos del búfer
[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; } }
Cree una instancia de la siguiente clase auxiliar para generar
RawVideoFrame
aleatorios conVideoStreamPixelFormat.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; } } }
Suscríbase al
VideoStream.StateChanged
delegado. Este evento informa del estado de la secuencia actual. No envíe fotogramas si el estado no es 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 compartiendo pantalla
Dado que el sistema Windows genera los fotogramas, debe implementar su propio servicio en primer plano para capturarlos y enviarlos mediante la API de llamadas de Azure Communication Services.
Resoluciones de vídeo admitidas
Relación de aspecto | Solución | FPS máximo |
---|---|---|
Cualquiera | Cualquier elemento hasta 1080p | 30 |
Pasos para crear una secuencia de vídeo de uso compartido de pantalla
- Cree una matriz de
VideoFormat
con el VideoStreamPixelFormat compatible con el SDK. Cuando hay varios formatos disponibles, el orden de los formatos en la lista no influye ni da prioridad a cuál se utiliza. Los criterios de selección de formato se basan en factores externos, como el ancho de banda de red.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 };
- Cree
RawOutgoingVideoStreamOptions
y configureVideoFormats
con el objeto creado anteriormente.var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions { Formats = videoStreamFormats };
- Cree una instancia de
VirtualOutgoingVideoStream
mediante la instanciaRawOutgoingVideoStreamOptions
que creó anteriormente.var rawOutgoingVideoStream = new ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions);
- Capture y envíe el fotograma de vídeo de la siguiente manera.
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 entrante sin procesar
Esta característica le proporciona acceso a los fotogramas de vídeo dentro de los objetos IncomingVideoStream
para manipular esas secuencias localmente
- Cree una instancia de
IncomingVideoOptions
que establece medianteJoinCallOptions
la configuración deVideoStreamKind.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 };
- Una vez que reciba un evento
ParticipantsUpdatedEventArgs
, adjunte el delegadoRemoteParticipant.VideoStreamStateChanged
. Este evento informa del estado de los objetos deIncomingVideoStream
.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; } }
- En ese momento,
IncomingVideoStream
tiene un estadoVideoStreamState.Available
con el delegadoRawIncomingVideoStream.RawVideoFrameReceived
adjuntado, tal y como se muestra en el paso anterior. Esto proporciona los nuevos objetos deRawVideoFrame
.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; } }
En este inicio rápido aprenderá a implementar el acceso a elementos multimedia sin procesar con el SDK de llamadas de Azure Communication Services para Android.
El SDK de llamadas de Azure Communication Services ofrece API que permiten a las aplicaciones generar sus propios fotogramas de vídeo para enviarlos a los participantes remotos en una llamada.
Este inicio rápido se basa en Inicio rápido: Adición de llamadas de vídeo 1:1 a la aplicación para Android.
Acceso a audio sin procesar
El acceso a medios de audio sin procesar proporciona acceso a la secuencia de audio entrante de la llamada, junto con la capacidad de ver y enviar secuencias de audio salientes personalizadas durante una llamada.
Enviar audio saliente sin procesar
Crear un objeto de opciones que especifique las propiedades de secuencia sin procesar 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);
Cree una instancia de RawOutgoingAudioStream
y adjúntela para unir las opciones de llamada y la secuencia se iniciará automáticamente cuando se conecte la llamada.
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.
Adjuntar secuencia a una llamada
También puede adjuntar la secuencia a una instancia existente de Call
en su lugar:
CompletableFuture<Void> result = call.startAudio(context, rawOutgoingAudioStream);
Empezar a enviar ejemplos sin procesar
Solo podemos empezar a enviar datos una vez que el estado de la secuencia sea AudioStreamState.STARTED
.
Para observar el cambio de estado de la secuencia de audio, agregue un agente de escucha al evento de OnStateChangedListener
.
private void onStateChanged(PropertyChangedEvent propertyChangedEvent) {
// When value is `AudioStreamState.STARTED` we'll be able to send audio samples.
}
rawOutgoingAudioStream.addOnStateChangedListener(this::onStateChanged)
Cuando se inicia la secuencia, podemos empezar a enviar ejemplos de audio de java.nio.ByteBuffer
a la llamada.
El formato del búfer de audio debe coincidir con las propiedades de la secuencia 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();
Recibir audio entrante sin procesar
También podemos recibir los ejemplos de secuencias de audio de llamada como java.nio.ByteBuffer
si queremos procesar el audio antes de la reproducción.
Crear un objeto de RawIncomingAudioStreamOptions
que especifique las propiedades de secuencia sin procesar que queremos recibir.
RawIncomingAudioStreamOptions options = new RawIncomingAudioStreamOptions();
RawIncomingAudioStreamProperties properties = new RawIncomingAudioStreamProperties()
.setAudioFormat(AudioStreamFormat.PCM16_BIT)
.setSampleRate(AudioStreamSampleRate.HZ44100)
.setChannelMode(AudioStreamChannelMode.STEREO);
options.setProperties(properties);
Cree una instancia de RawIncomingAudioStream
y adjúntela para unir las opciones de llamada
JoinCallOptions options = JoinCallOptions() // or StartCallOptions()
IncomingAudioOptions incomingAudioOptions = new IncomingAudioOptions();
RawIncomingAudioStream rawIncomingAudioStream = new RawIncomingAudioStream(audioStreamOptions);
incomingAudioOptions.setStream(rawIncomingAudioStream);
options.setIncomingAudioOptions(incomingAudioOptions);
También podemos adjuntar la secuencia a una instancia existente de Call
en su lugar:
CompletableFuture<Void> result = call.startAudio(context, rawIncomingAudioStream);
Para empezar a recibir búferes de audio sin procesar de la secuencia entrante, agregue agentes de escucha al estado de la secuencia entrante y a los eventos recibidos del búfer.
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);
También es importante recordar detener la secuencia de audio en la instancia de Call
de llamada actual:
CompletableFuture<Void> result = call.stopAudio(context, rawIncomingAudioStream);
Acceso a vídeos sin procesar
Puesto que la aplicación genera los fotogramas de vídeo, esta debe informar al SDK de llamadas de Azure Communication Services sobre los formatos de vídeo que la aplicación es capaz de generar. Esta información permite que el SDK de llamadas de Azure Communication Services elija la mejor configuración de formato de vídeo dadas las condiciones de red en un momento determinado.
Vídeo virtual
Resoluciones de vídeo admitidas
Relación de aspecto | Solución | 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 |
Cree una matriz de
VideoFormat
con el VideoStreamPixelFormat compatible con el SDK.Cuando hay varios formatos disponibles, el orden de los formatos en la lista no influye ni da prioridad a cuál se utiliza. Los criterios de selección de formato se basan en factores externos, como el ancho de banda de red.
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);
Cree
RawOutgoingVideoStreamOptions
y configureFormats
con el objeto creado anteriormente.RawOutgoingVideoStreamOptions rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions(); rawOutgoingVideoStreamOptions.setFormats(videoStreamFormats);
Cree una instancia de
VirtualOutgoingVideoStream
mediante la instanciaRawOutgoingVideoStreamOptions
que creó anteriormente.VirtualOutgoingVideoStream rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
Suscríbase al
RawOutgoingVideoStream.addOnFormatChangedListener
delegado. Este evento informa siempre que se cambieVideoStreamFormat
de uno de los formatos de vídeo proporcionados en la lista.virtualOutgoingVideoStream.addOnFormatChangedListener((VideoStreamFormatChangedEvent args) -> { VideoStreamFormat videoStreamFormat = args.Format; });
Cree una instancia de la siguiente clase auxiliar para generar
RawVideoFrame
aleatorios conVideoStreamPixelFormat.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(); } } }
Suscríbase al
VideoStream.addOnStateChangedListener
delegado. Este delegado informa del estado de la secuencia actual. No envíe fotogramas si el estado no es 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 de pantalla compartida
Dado que el sistema Windows genera los fotogramas, debe implementar su propio servicio en primer plano para capturarlos y enviarlos mediante la API de llamadas de Azure Communication Services.
Resoluciones de vídeo admitidas
Relación de aspecto | Solución | FPS máximo |
---|---|---|
Cualquiera | Cualquier elemento hasta 1080p | 30 |
Pasos para crear una secuencia de vídeo de uso compartido de pantalla
Cree una matriz de
VideoFormat
con el VideoStreamPixelFormat compatible con el SDK.Cuando hay varios formatos disponibles, el orden de los formatos en la lista no influye ni da prioridad a cuál se utiliza. Los criterios de selección de formato se basan en factores externos, como el ancho de banda de red.
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);
Cree
RawOutgoingVideoStreamOptions
y configureVideoFormats
con el objeto creado anteriormente.RawOutgoingVideoStreamOptions rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions(); rawOutgoingVideoStreamOptions.setFormats(videoStreamFormats);
Cree una instancia de
VirtualOutgoingVideoStream
mediante la instanciaRawOutgoingVideoStreamOptions
que creó anteriormente.ScreenShareOutgoingVideoStream rawOutgoingVideoStream = new ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions);
Capture y envíe el fotograma de vídeo de la siguiente manera.
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 entrante sin procesar
Esta característica le proporciona acceso a los fotogramas de vídeo dentro de los objetos de IncomingVideoStream
para manipular localmente esos objetos de secuencia
Cree una instancia de
IncomingVideoOptions
que establece medianteJoinCallOptions
la configuración deVideoStreamKind.RawIncoming
IncomingVideoOptions incomingVideoOptions = new IncomingVideoOptions() .setStreamType(VideoStreamKind.RAW_INCOMING); JoinCallOptions joinCallOptions = new JoinCallOptions() .setIncomingVideoOptions(incomingVideoOptions);
Una vez que reciba un evento
ParticipantsUpdatedEventArgs
, adjunte el delegadoRemoteParticipant.VideoStreamStateChanged
. Este evento informa del estado del objeto deIncomingVideoStream
.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; } }
En ese momento,
IncomingVideoStream
tiene un estadoVideoStreamState.Available
con el delegadoRawIncomingVideoStream.RawVideoFrameReceived
adjuntado, tal y como se muestra en el paso anterior. Ese delegado proporciona los nuevos objetos deRawVideoFrame
.private void OnVideoFrameReceived(RawVideoFrameReceivedEventArgs args) { // Render/Modify/Save the video frame RawVideoFrameBuffer videoFrame = (RawVideoFrameBuffer) args.getFrame(); }
En este inicio rápido, aprenderá a implementar el acceso a elementos multimedia sin procesar con el SDK de llamadas de Azure Communication Services para iOS.
El SDK de llamadas de Azure Communication Services ofrece API que permiten a las aplicaciones generar sus propios fotogramas de vídeo para enviarlos a los participantes remotos en una llamada.
Este inicio rápido se basa en el Inicio rápido: Adición de llamadas de vídeo 1:1 a la aplicación para iOS.
Acceso a audio sin procesar
El acceso a medios de audio sin procesar proporciona acceso a la secuencia de audio de la llamada entrante, junto con la capacidad de ver y enviar secuencias de audio salientes personalizadas durante una llamada.
Enviar audio saliente sin procesar
Crear un objeto de opciones que especifique las propiedades de secuencia sin procesar que queremos enviar.
let outgoingAudioStreamOptions = RawOutgoingAudioStreamOptions()
let properties = RawOutgoingAudioStreamProperties()
properties.sampleRate = .hz44100
properties.bufferDuration = .inMs20
properties.channelMode = .mono
properties.format = .pcm16Bit
outgoingAudioStreamOptions.properties = properties
Cree una instancia de RawOutgoingAudioStream
y adjúntela para unir las opciones de llamada y la secuencia se iniciará automáticamente cuando se conecte la llamada.
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.
Adjuntar secuencia a una llamada
También puede adjuntar la secuencia a una instancia existente de Call
en su lugar:
call.startAudio(stream: self.rawOutgoingAudioStream) { error in
// Stream attached to `Call`.
}
Empiece a enviar ejemplos sin procesar
Solo podemos empezar a enviar datos una vez que el estado de la secuencia sea AudioStreamState.started
.
Para observar el cambio de estado de la secuencia de audio, implementamos RawOutgoingAudioStreamDelegate
. Establecer como delegado de secuencia.
func rawOutgoingAudioStream(_ rawOutgoingAudioStream: RawOutgoingAudioStream,
didChangeState args: AudioStreamStateChangedEventArgs) {
// When value is `AudioStreamState.started` we will be able to send audio samples.
}
self.rawOutgoingAudioStream.delegate = DelegateImplementer()
o usar la clausura basada en
self.rawOutgoingAudioStream.events.onStateChanged = { args in
// When value is `AudioStreamState.started` we will be able to send audio samples.
}
Cuando se inicia la secuencia, podemos empezar a enviar ejemplos de audio de AVAudioPCMBuffer
a la llamada.
El formato del búfer de audio debe coincidir con las propiedades de la secuencia 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()
}
}
También es importante recordar detener la secuencia de audio en la instancia de Call
de llamada actual:
call.stopAudio(stream: self.rawOutgoingAudioStream) { error in
// Stream detached from `Call` and stopped.
}
Captura de ejemplos de micrófono
Con AVAudioEngine
de Apple podemos capturar fotogramas de micrófono pulsando en el nodo de entrada del motor de audio. Y al capturar los datos del micrófono y poder utilizar la funcionalidad de audio sin procesar, podemos procesar el audio antes de enviarlo a una llamada.
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
La frecuencia de muestreo del nodo de entrada del motor de audio tiene como de forma predeterminada un >valor de la frecuencia de muestreo preferida para la sesión de audio compartida. Por lo tanto, no podemos instalar la pulsación en ese nodo con un valor diferente.
Por ello, tenemos que asegurarnos de que la frecuencia de muestreo de propiedades RawOutgoingStream
coincida con la que obtenemos de los ejemplos de micrófono o convertir los búferes de pulsación al formato que coincide con lo esperado en la secuencia saliente.
Con este pequeño ejemplo, hemos aprendido cómo podemos capturar los datos AVAudioEngine
del micrófono y enviar esos ejemplos a una llamada mediante la característica de audio saliente sin procesar.
Recibir audio entrante sin procesar
También podemos recibir los ejemplos de secuencias de audio de llamada como AVAudioPCMBuffer
si queremos procesar el audio antes de la reproducción.
Crear un objeto de RawIncomingAudioStreamOptions
que especifique las propiedades de secuencia sin procesar que queremos recibir.
let options = RawIncomingAudioStreamOptions()
let properties = RawIncomingAudioStreamProperties()
properties.format = .pcm16Bit
properties.sampleRate = .hz44100
properties.channelMode = .stereo
options.properties = properties
Cree una instancia de RawOutgoingAudioStream
y adjúntela para unir las opciones de llamada
let options = JoinCallOptions() // or StartCallOptions()
let incomingAudioOptions = IncomingAudioOptions()
self.rawIncomingStream = RawIncomingAudioStream(rawIncomingAudioStreamOptions: audioStreamOptions)
incomingAudioOptions.stream = self.rawIncomingStream
options.incomingAudioOptions = incomingAudioOptions
También podemos adjuntar la secuencia a una instancia existente de Call
en su lugar:
call.startAudio(stream: self.rawIncomingStream) { error in
// Stream attached to `Call`.
}
Para empezar a recibir el búfer de audio sin procesar de la secuencia entrante, implemente 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()
o
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.
}
Acceso a vídeos sin procesar
Puesto que la aplicación genera los fotogramas de vídeo, esta debe informar al SDK de llamadas de Azure Communication Services sobre los formatos de vídeo que la aplicación es capaz de generar. Esta información permite que el SDK de llamadas de Azure Communication Services elija la mejor configuración de formato de vídeo dadas las condiciones de red en un momento determinado.
Vídeo virtual
Resoluciones de vídeo admitidas
Relación de aspecto | Solución | 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 |
Cree una matriz de
VideoFormat
con el VideoStreamPixelFormat compatible con el SDK. Cuando hay varios formatos disponibles, el orden de los formatos en la lista no influye ni da prioridad a cuál se utiliza. Los criterios de selección de formato se basan en factores externos, como el ancho de banda de red.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)
Cree
RawOutgoingVideoStreamOptions
y configure formatos con el objeto creado anteriormente.var rawOutgoingVideoStreamOptions = RawOutgoingVideoStreamOptions() rawOutgoingVideoStreamOptions.formats = videoStreamFormats
Cree una instancia de
VirtualOutgoingVideoStream
mediante la instanciaRawOutgoingVideoStreamOptions
que creó anteriormente.var rawOutgoingVideoStream = VirtualOutgoingVideoStream(videoStreamOptions: rawOutgoingVideoStreamOptions)
Implemente al delegado de
VirtualOutgoingVideoStreamDelegate
. El evento dedidChangeFormat
informa siempre que se ha cambiadoVideoStreamFormat
de uno de los formatos de vídeo proporcionados en la lista.virtualOutgoingVideoStream.delegate = /* Attach delegate and implement didChangeFormat */
Cree una instancia de la siguiente clase auxiliar para acceder a los datos de
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 } }
Cree una instancia de la siguiente clase auxiliar para generar
RawVideoFrameBuffer
aleatorios conVideoStreamPixelFormat.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 en
VirtualOutgoingVideoStreamDelegate
. El evento dedidChangeState
informa del estado de la secuencia actual. No envíe fotogramas si el estado no es 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 de pantalla compartida
Dado que el sistema Windows genera los fotogramas, debe implementar su propio servicio en primer plano para capturarlos y enviarlos mediante la API de llamadas de Azure Communication Services.
Resoluciones de vídeo admitidas
Relación de aspecto | Solución | FPS máximo |
---|---|---|
Cualquiera | Cualquier elemento hasta 1080p | 30 |
Pasos para crear una secuencia de vídeo de uso compartido de pantalla
Cree una matriz de
VideoFormat
con el VideoStreamPixelFormat compatible con el SDK. Cuando hay varios formatos disponibles, el orden de los formatos en la lista no influye ni da prioridad a cuál se utiliza. Los criterios de selección de formato se basan en factores externos, como el ancho de banda de red.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)
Cree
RawOutgoingVideoStreamOptions
y configureVideoFormats
con el objeto creado anteriormente.var rawOutgoingVideoStreamOptions = RawOutgoingVideoStreamOptions() rawOutgoingVideoStreamOptions.formats = videoStreamFormats
Cree una instancia de
VirtualOutgoingVideoStream
mediante la instanciaRawOutgoingVideoStreamOptions
que creó anteriormente.var rawOutgoingVideoStream = ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions)
Capture y envíe el fotograma de vídeo de la siguiente manera.
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 entrante sin procesar
Esta característica le proporciona acceso a los fotogramas de vídeo dentro de los IncomingVideoStream
para manipular localmente esos objetos de secuencia
Cree una instancia de
IncomingVideoOptions
que establece medianteJoinCallOptions
la configuración deVideoStreamKind.RawIncoming
var incomingVideoOptions = IncomingVideoOptions() incomingVideoOptions.streamType = VideoStreamKind.rawIncoming var joinCallOptions = JoinCallOptions() joinCallOptions.incomingVideoOptions = incomingVideoOptions
Una vez que reciba un evento
ParticipantsUpdatedEventArgs
, adjunte el delegadoRemoteParticipant.delegate.didChangedVideoStreamState
. Este evento informa del estado de los objetos deIncomingVideoStream
.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 } }
En ese momento,
IncomingVideoStream
tiene un estadoVideoStreamState.available
con el delegadoRawIncomingVideoStream.delegate.didReceivedRawVideoFrame
adjuntado, tal y como se muestra en el paso anterior. Este evento proporciona los nuevos objetos deRawVideoFrame
.func rawIncomingVideoStream(_ rawIncomingVideoStream: RawIncomingVideoStream, didRawVideoFrameReceived args: RawVideoFrameReceivedEventArgs) { /* Render/Modify/Save the video frame */ let videoFrame = args.frame as! RawVideoFrameBuffer }
Como desarrollador, puede acceder a los medios sin procesar para el contenido entrante y saliente de audio, vídeo y uso compartido de pantalla durante una llamada para poder capturar, analizar y procesar contenido de audio y vídeo. El acceso al audio sin procesar, al vídeo sin procesar y a los recursos compartidos de pantalla sin procesar del lado cliente de Azure Communication Services ofrece a los desarrolladores una capacidad casi ilimitada de ver y editar contenido de audio, vídeo y recursos compartidos de pantalla que se produce dentro del SDK de llamadas de Azure Communication Services. En este inicio rápido, aprenderá a implementar el acceso a elementos multimedia sin procesar con el SDK de llamadas de Azure Communication Services para JavaScript.
Por ejemplo,
- Puede acceder a la secuencia de vídeo y audio de la llamada entrante directamente en el objeto de llamada y enviar secuencias de vídeo y audio salientes personalizadas durante la llamada.
- Puede inspeccionar secuencias de audio y vídeo para ejecutar modelos de IA personalizados para su análisis. Estos modelos pueden incluir el procesamiento de lenguaje natural para analizar conversaciones o proporcionar información y sugerencias en tiempo real para aumentar la productividad del agente.
- Las organizaciones pueden usar secuencias multimedia de audio y vídeo para analizar la opinión al proporcionar atención virtual a los pacientes o para proporcionar asistencia remota durante las videollamadas que usan realidad mixta. Esto capacidad abre una ruta para que los desarrolladores apliquen las innovaciones para mejorar las experiencias de interacción.
Requisitos previos
Importante
Estos ejemplos están disponibles en 1.13.1 del SDK de llamada para JavaScript. Asegúrese de usar esa versión o posterior al probar este inicio rápido.
Acceso al audio sin procesar
El acceso a medios de audio sin procesar proporciona acceso a la secuencia de audio de la llamada entrante, junto con la capacidad de ver y enviar secuencias de audio salientes personalizadas durante una llamada.
Acceso a una secuencia de audio entrante sin procesar
Use el código siguiente para acceder a la secuencia de audio de una llamada entrante.
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);
Realice una llamada con una secuencia de audio personalizada
Use el código siguiente para iniciar una llamada con una secuencia de audio personalizada en lugar de usar el dispositivo de micrófono de un usuario.
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);
Cambio a una secuencia de audio personalizada durante una llamada
Use el código siguiente para cambiar el dispositivo de entrada a una secuencia de audio personalizada en lugar de usar el dispositivo de micrófono del usuario durante una llamada.
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);
Detener una secuencia de audio personalizada
Use el código siguiente para dejar de enviar una secuencia de audio personalizada después de que se haya establecido durante una llamada.
call.stopAudio();
Acceso al vídeo sin procesar
Los medios de vídeo sin procesar proporcionan la instancia de un objeto MediaStream
. (Para obtener más información, consulte la documentación de JavaScript). Los medios de vídeo sin procesar proporcionan acceso específicamente al objeto MediaStream
para las llamadas entrantes y salientes. En el caso del vídeo sin procesar, puede usar ese objeto para aplicar filtros mediante el aprendizaje automático para procesar fotogramas del vídeo.
Los fotogramas de vídeo salientes sin formato procesados se pueden enviar como vídeo saliente del remitente. Los fotogramas de vídeo entrantes procesados sin formato se pueden representar en el lado receptor.
Realice una llamada con una secuencia de vídeo personalizada
Puede acceder a la secuencia de vídeo sin procesar para una llamada saliente. Use MediaStream
para la secuencia de vídeo sin formato saliente para procesar fotogramas mediante el aprendizaje automático y para aplicar filtros. A continuación, el vídeo saliente procesado se puede enviar como secuencia de vídeo remitente.
En este ejemplo se envían datos de lienzo a un usuario como vídeo saliente.
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);
Cambio a una secuencia de vídeo personalizada durante una llamada
Use el código siguiente para cambiar un dispositivo de entrada a una secuencia de vídeo personalizada en lugar de usar el dispositivo de cámara de un usuario durante una llamada.
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);
Detener una secuencia de vídeo personalizada
Use el código siguiente para dejar de enviar una secuencia de vídeo personalizada después de que se haya establecido durante una llamada.
// Stop video by passing the same `localVideoStream` instance that was used to start video
await call.stopVideo(localVideoStream);
Al cambiar de una cámara que tiene efectos personalizados aplicados a otro dispositivo de cámara, detenga primero el vídeo, cambie el origen en LocalVideoStream
y vuelva a iniciar el vídeo.
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);
}
Acceder al flujo de vídeo entrante de un participante remoto
Puede acceder a la secuencia de vídeo sin procesar para una llamada entrante. Use MediaStream
para la secuencia de vídeo sin formato entrante para procesar fotogramas mediante el aprendizaje automático y para aplicar filtros. A continuación, el vídeo entrante procesado se puede representar en el lado receptor.
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 característica de Azure Communication Services se encuentra actualmente en versión preliminar.
Las API y los SDK en versión preliminar se proporcionan sin contrato de nivel de servicio. Se recomienda no usarlos para las cargas de trabajo de producción. Es posible que algunas características no sean compatibles o que sus funcionalidades estén limitadas.
Para obtener más información, consulte Términos de uso complementarios para las Versiones preliminares de Microsoft Azure.
El acceso a los recursos compartidos de pantalla sin formato está en versión preliminar pública y está disponible como parte de la versión 1.15.1-beta.1+.
Acceso a los recursos compartidos de pantalla sin formato
Los medios de recursos compartidos de pantalla sin formato proporcionan acceso específicamente al objeto de MediaStream
para las secuencias de recursos compartidos de pantalla entrantes y salientes. En el caso de los recursos compartidos de pantalla sin procesar, puede usar ese objeto para aplicar filtros mediante el aprendizaje automático para procesar fotogramas del recurso compartido de pantalla.
Los fotogramas de los recursos compartidos de pantalla salientes sin formato y procesados se pueden enviar como recurso compartido de pantalla saliente del remitente. Los fotogramas de recursos compartidos de pantalla entrantes procesados y sin formato se pueden representar en el lado receptor.
Nota: El envío de recursos compartidos de pantalla solo se admite en el explorador de escritorio.
Inicio del recurso compartido de pantalla con una secuencia de recursos compartidos de pantalla personalizada
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'
Acceda a la secuencia de recursos compartidos de pantalla sin procesar desde una pantalla, una pestaña del explorador o una aplicación y aplique efectos a la secuencia
A continuación se muestra un ejemplo de cómo aplicar un efecto de blanco y negro al flujo de pantalla compartida sin procesar de una pantalla, pestaña del navegador o aplicación. NOTA: La API canvas context filter = "grayscale(1)" API no se admite en 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;
Detener el envío de secuencias de recursos compartidos de pantalla
Use el código siguiente para dejar de enviar una secuencia de recursos compartidos de pantalla personalizada después de que se haya establecido durante una llamada.
// Stop sending raw screen sharing stream
await call.stopScreenSharing(localScreenSharingStream);
Acceso a la secuencia de recursos compartidos de pantalla entrante desde un participante remoto
Puede acceder a la secuencia de recursos compartidos de pantalla sin procesar desde un participante remoto. Use MediaStream
para la secuencia de recursos compartidos de pantalla sin formato entrante para procesar fotogramas mediante el aprendizaje automático y para aplicar filtros. A continuación, el recurso compartido de pantalla entrante procesado se puede representar en el lado 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();
Pasos siguientes
Para más información, consulte los siguientes artículos.
- Consulte nuestra muestra de elementos principales de una llamada.
- Introducción a la biblioteca de la interfaz de usuario.
- Más información sobre las Funcionalidades del SDK de llamadas.
- Más información sobre cómo funcionan las llamadas.