Démarrage rapide : ajouter l’accès aux médias bruts à votre application
Dans ce démarrage rapide, vous allez découvrir comment implémenter l’accès aux médias bruts à l’aide du SDK Azure Communication Services Calling pour Unity. Le SDK Azure Communication Services Calling fournit des API qui permettent aux applications de générer leurs propres images vidéo pour envoyer ou rendre des images vidéo brutes provenant de participants distants à un appel. Ce démarrage rapide s’appuie sur le guide Démarrage rapide : Ajouter l’appel vidéo individuel à votre application pour Unity.
Accès à RawVideo
Étant donné que l’application génère les images vidéo, elle doit indiquer au SDK Azure Communication Services Calling les formats vidéo qu’elle peut générer. Grâce à cette information, le SDK Azure Communication Services Calling peut choisir la meilleure configuration de format vidéo selon les conditions du réseau à ce moment-là.
Vidéo virtuelle
Résolutions vidéo prises en charge
Proportions | Résolution | FPS maximum |
---|---|---|
16x9 | 1080 p | 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 |
Suivez les étapes décrites ici Démarrage rapide : ajoutez un appel vidéo individuel à votre application pour créer un jeu Unity. L’objectif est d’obtenir un objet
CallAgent
prêt à commencer l’appel. Vous trouverez le code finalisé pour ce guide de démarrage rapide sur GitHub.Créez un tableau de
VideoFormat
à l’aide de VideoStreamPixelFormat que le SDK prend en charge. Lorsque plusieurs formats sont disponibles, leur ordre dans la liste ne détermine pas celui qui est utilisé. Les critères de sélection du format dépendent de facteurs externes tels que la bande passante réseau.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 };
Créez
RawOutgoingVideoStreamOptions
et définissezFormats
avec les objets créés précédemment.var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions { Formats = videoStreamFormats };
Créez une instance de
VirtualOutgoingVideoStream
à l’aide de l’instanceRawOutgoingVideoStreamOptions
que vous avez créée précédemment.var rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
Abonnez-vous au délégué
RawOutgoingVideoStream.FormatChanged
. Cet événement informe chaque fois que leVideoStreamFormat
a été modifié à partir de l’un des formats vidéo fournis dans la liste.rawOutgoingVideoStream.FormatChanged += (object sender, VideoStreamFormatChangedEventArgs args) { VideoStreamFormat videoStreamFormat = args.Format; }
Abonnez-vous au délégué
RawOutgoingVideoStream.StateChanged
. Cet événement indique chaque fois queState
a changé.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; } }
Traiter les transactions d'état du flux vidéo brut sortant, telles que le démarrage et l'arrêt, et commencer à générer des images vidéo personnalisées ou suspendre l'algorithme de génération d'images.
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; } }
Voici un exemple de générateur de trames vidéo sortant :
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 }; }
Remarque
Le modificateur
unsafe
est utilisé sur cette méthode, carNativeBuffer
nécessite l’accès aux ressources de mémoire natives. Par conséquent, l’optionAllow unsafe
doit également être activée dans l’Éditeur Unity.De même, nous pouvons gérer les images vidéo entrantes en réponse à l’événement de flux vidéo
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 }); }
Il est vivement recommandé de gérer à la fois les images vidéo entrantes et sortantes par le biais d’un mécanisme de mise en mémoire tampon pour éviter la surcharge de la méthode de rappel
MonoBehaviour.Update()
, qui doit être conservée légère et éviter les tâches intensives du processeur ou du réseau et garantir une expérience vidéo plus fluide. Cette optimisation facultative est laissée à l'appréciation des développeurs, qui décident de ce qui fonctionne le mieux dans leurs scénarios.Voici un exemple de rendu des images entrantes dans une unité
VideoTexture
en appelantGraphics.Blit
à partir d’une file d’attente interne :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(); } }
Dans ce démarrage rapide, vous allez découvrir comment implémenter l’accès aux médias bruts en utilisant le SDK Azure Communication Services Calling pour Windows. Le SDK Azure Communication Services Calling fournit des API qui permettent aux applications de générer leurs propres images vidéo pour les envoyer ensuite aux participants distants à un appel. Ce démarrage rapide s’appuie sur le guide Démarrage rapide : Ajouter l’appel vidéo 1 à 1 à votre application pour Windows.
Accès à RawAudio
L’accès à l’audio brut permet d’accéder au flux audio d’un appel entrant et d’afficher et envoyer un flux audio sortant personnalisé lors d’un appel.
Envoyer de l’audio sortant brut
Créez un objet d’options spécifiant les propriétés de flux brut que nous voulons envoyer.
RawOutgoingAudioStreamProperties outgoingAudioProperties = new RawOutgoingAudioStreamProperties()
{
Format = ACSAudioStreamFormat.Pcm16Bit,
SampleRate = AudioStreamSampleRate.Hz48000,
ChannelMode = AudioStreamChannelMode.Stereo,
BufferDuration = AudioStreamBufferDuration.InMs20
};
RawOutgoingAudioStreamOptions outgoingAudioStreamOptions = new RawOutgoingAudioStreamOptions()
{
Properties = outgoingAudioProperties
};
Créez un RawOutgoingAudioStream
et attachez-le aux options d’appel de jointure et le flux démarre automatiquement lorsque l’appel est connecté.
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.
Attacher un flux à un appel
Vous pouvez également attacher le flux à une instance de Call
existante à la place :
await call.StartAudio(rawOutgoingAudioStream);
Commencer à envoyer des échantillons bruts
Nous ne pouvons commencer à envoyer des données qu’une fois que l’état du flux est AudioStreamState.Started
.
Pour observer le changement d’état du flux audio, ajoutez un écouteur à l’événement OnStateChangedListener
.
unsafe private void AudioStateChanged(object sender, AudioStreamStateChanged args)
{
if (args.AudioStreamState == AudioStreamState.Started)
{
// We can now start sending samples.
}
}
outgoingAudioStream.StateChanged += AudioStateChanged;
Lorsque le flux a démarré, nous pouvons commencer à envoyer des échantillons audio MemoryBuffer
à l’appel.
Le format de mémoire tampon audio doit correspondre aux propriétés de flux spécifiées.
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();
}
Recevoir de l’audio entrant brut
Nous pouvons également recevoir les échantillons de flux audio d’appel comme MemoryBuffer
si nous voulions traiter le flux audio de l’appel avant la lecture.
Créez un objet RawIncomingAudioStreamOptions
spécifiant les propriétés de flux brut que nous voulons recevoir.
RawIncomingAudioStreamProperties properties = new RawIncomingAudioStreamProperties()
{
Format = AudioStreamFormat.Pcm16Bit,
SampleRate = AudioStreamSampleRate.Hz44100,
ChannelMode = AudioStreamChannelMode.Stereo
};
RawIncomingAudioStreamOptions options = new RawIncomingAudioStreamOptions()
{
Properties = properties
};
Créer un RawIncomingAudioStream
et l’attacher aux options d’appel de jointure
JoinCallOptions options = JoinCallOptions(); // or StartCallOptions()
RawIncomingAudioStream rawIncomingAudioStream = new RawIncomingAudioStream(audioStreamOptions);
IncomingAudioOptions incomingAudioOptions = new IncomingAudioOptions()
{
Stream = rawIncomingAudioStream
};
options.IncomingAudioOptions = incomingAudioOptions;
Nous pouvons également attacher le flux à une instance de Call
existante à la place :
await call.startAudio(context, rawIncomingAudioStream);
Pour commencer à recevoir des mémoires tampons audio brutes à partir du flux entrant, ajoutez des écouteurs à l’état du flux entrant et aux événements de mémoire tampon reçus.
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;
Accès à RawVideo
Étant donné que l’application génère les images vidéo, elle doit indiquer au SDK Azure Communication Services Calling les formats vidéo qu’elle peut générer. Grâce à cette information, le SDK Azure Communication Services Calling peut choisir la meilleure configuration de format vidéo selon les conditions du réseau à ce moment-là.
Vidéo virtuelle
Résolutions vidéo prises en charge
Proportions | Résolution | FPS maximum |
---|---|---|
16x9 | 1080 p | 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 |
Créez un tableau de
VideoFormat
à l’aide de VideoStreamPixelFormat que le SDK prend en charge. Lorsque plusieurs formats sont disponibles, leur ordre dans la liste ne détermine pas celui qui est utilisé. Les critères de sélection du format dépendent de facteurs externes tels que la bande passante réseau.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 };
Créez
RawOutgoingVideoStreamOptions
et définissezFormats
avec les objets créés précédemment.var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions { Formats = videoStreamFormats };
Créez une instance de
VirtualOutgoingVideoStream
à l’aide de l’instanceRawOutgoingVideoStreamOptions
que vous avez créée précédemment.var rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
Abonnez-vous au délégué
RawOutgoingVideoStream.FormatChanged
. Cet événement informe chaque fois que leVideoStreamFormat
a été modifié à partir de l’un des formats vidéo fournis dans la liste.rawOutgoingVideoStream.FormatChanged += (object sender, VideoStreamFormatChangedEventArgs args) { VideoStreamFormat videoStreamFormat = args.Format; }
Créer une instance de la classe d’assistance suivante pour accéder aux données de mémoire tampon
[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; } }
Créez une instance de la classe d’assistance suivante pour générer des données
RawVideoFrame
aléatoires à l’aide 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; } } }
Abonnez-vous au délégué
VideoStream.StateChanged
. Cet événement informe l’état du flux actuel. N’envoyez pas d’images si l’état n’est pas égal àVideoStreamState.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; } };
Vidéo de partage d’écran
Étant donné que le système Windows génère les images, vous devez implémenter votre propre service de premier plan pour capturer les images et les diffuser à l’aide notre API Azure Communication Services Calling.
Résolutions vidéo prises en charge
Proportions | Résolution | FPS maximum |
---|---|---|
Tout | N’importe quelle valeur jusqu’à 1080p | 30 |
Étapes de la création d’un flux vidéo de partage d’écran
- Créez un tableau de
VideoFormat
à l’aide de VideoStreamPixelFormat que le SDK prend en charge. Lorsque plusieurs formats sont disponibles, leur ordre dans la liste ne détermine pas celui qui est utilisé. Les critères de sélection du format dépendent de facteurs externes tels que la bande passante réseau.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 };
- Créez
RawOutgoingVideoStreamOptions
et définissezVideoFormats
avec les objets créés précédemment.var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions { Formats = videoStreamFormats };
- Créez une instance de
VirtualOutgoingVideoStream
à l’aide de l’instanceRawOutgoingVideoStreamOptions
que vous avez créée précédemment.var rawOutgoingVideoStream = new ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions);
- Capturez et envoyez l’image vidéo de la manière suivante.
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; } }
Vidéo entrante brute
Cette fonctionnalité vous permet d’accéder aux images vidéo à l’intérieur du IncomingVideoStream
afin de manipuler ces flux localement
- Créer une instance de
IncomingVideoOptions
qui définitJoinCallOptions
en paramétrantVideoStreamKind.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 };
- Une fois que vous recevez un délégué
RemoteParticipant.VideoStreamStateChanged
d’attachement d’événementParticipantsUpdatedEventArgs
. Cet événement informe l’état des objetsIncomingVideoStream
.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; } }
- À ce moment,
IncomingVideoStream
a un déléguéRawIncomingVideoStream.RawVideoFrameReceived
d’attachement d’étatVideoStreamState.Available
, comme indiqué à l’étape précédente. Qui fournit les nouveaux objetsRawVideoFrame
.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; } }
Dans ce démarrage rapide, vous allez apprendre à implémenter l’accès aux médias bruts à l’aide du SDK Azure Communication Services Calling pour Android.
Le SDK Azure Communication Services Calling fournit des API qui permettent aux applications de générer leurs propres images vidéo pour les envoyer ensuite aux participants distants à un appel.
Ce démarrage rapide s’appuie sur le guide Démarrage rapide : Ajouter l’appel vidéo 1 à 1 à votre application pour Android.
Accès à RawAudio
L’accès à l’audio brut permet d’accéder au flux audio entrant de l’appel et d’afficher et envoyer un flux audio sortant personnalisé lors d’un appel.
Envoyer de l’audio sortant brut
Créez un objet d’options spécifiant les propriétés de flux brut que nous voulons envoyer.
RawOutgoingAudioStreamProperties outgoingAudioProperties = new RawOutgoingAudioStreamProperties()
.setAudioFormat(AudioStreamFormat.PCM16_BIT)
.setSampleRate(AudioStreamSampleRate.HZ44100)
.setChannelMode(AudioStreamChannelMode.STEREO)
.setBufferDuration(AudioStreamBufferDuration.IN_MS20);
RawOutgoingAudioStreamOptions outgoingAudioStreamOptions = new RawOutgoingAudioStreamOptions()
.setProperties(outgoingAudioProperties);
Créez un RawOutgoingAudioStream
et attachez-le aux options d’appel de jointure et le flux démarre automatiquement lorsque l’appel est connecté.
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.
Attacher un flux à un appel
Vous pouvez également attacher le flux à une instance de Call
existante à la place :
CompletableFuture<Void> result = call.startAudio(context, rawOutgoingAudioStream);
Commencer à envoyer des échantillons bruts
Nous ne pouvons commencer à envoyer des données qu’une fois que l’état du flux est AudioStreamState.STARTED
.
Pour observer le changement d’état du flux audio, ajoutez un écouteur à l’événement OnStateChangedListener
.
private void onStateChanged(PropertyChangedEvent propertyChangedEvent) {
// When value is `AudioStreamState.STARTED` we'll be able to send audio samples.
}
rawOutgoingAudioStream.addOnStateChangedListener(this::onStateChanged)
Lorsque le flux a démarré, nous pouvons commencer à envoyer des échantillons audio java.nio.ByteBuffer
à l’appel.
Le format de mémoire tampon audio doit correspondre aux propriétés de flux spécifiées.
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();
Recevoir de l’audio entrant brut
Nous pouvons également recevoir les échantillons de flux audio d’appel comme java.nio.ByteBuffer
si nous voulions traiter l’audio avant la lecture.
Créez un objet RawIncomingAudioStreamOptions
spécifiant les propriétés de flux brut que nous voulons recevoir.
RawIncomingAudioStreamOptions options = new RawIncomingAudioStreamOptions();
RawIncomingAudioStreamProperties properties = new RawIncomingAudioStreamProperties()
.setAudioFormat(AudioStreamFormat.PCM16_BIT)
.setSampleRate(AudioStreamSampleRate.HZ44100)
.setChannelMode(AudioStreamChannelMode.STEREO);
options.setProperties(properties);
Créer un RawIncomingAudioStream
et l’attacher aux options d’appel de jointure
JoinCallOptions options = JoinCallOptions() // or StartCallOptions()
IncomingAudioOptions incomingAudioOptions = new IncomingAudioOptions();
RawIncomingAudioStream rawIncomingAudioStream = new RawIncomingAudioStream(audioStreamOptions);
incomingAudioOptions.setStream(rawIncomingAudioStream);
options.setIncomingAudioOptions(incomingAudioOptions);
Nous pouvons également attacher le flux à une instance de Call
existante à la place :
CompletableFuture<Void> result = call.startAudio(context, rawIncomingAudioStream);
Pour commencer à recevoir des mémoires tampons audio brutes à partir du flux entrant, ajoutez des écouteurs à l’état du flux entrant et aux événements de mémoire tampon reçus.
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);
Il est également important de ne pas oublier d’arrêter le flux audio dans l’instance d’appel Call
actuelle :
CompletableFuture<Void> result = call.stopAudio(context, rawIncomingAudioStream);
Accès à RawVideo
Étant donné que l’application génère les images vidéo, elle doit indiquer au SDK Azure Communication Services Calling les formats vidéo qu’elle peut générer. Grâce à cette information, le SDK Azure Communication Services Calling peut choisir la meilleure configuration de format vidéo selon les conditions du réseau à ce moment-là.
Vidéo virtuelle
Résolutions vidéo prises en charge
Proportions | Résolution | FPS maximum |
---|---|---|
16x9 | 1080 p | 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 |
Créez un tableau de
VideoFormat
à l’aide de VideoStreamPixelFormat que le SDK prend en charge.Lorsque plusieurs formats sont disponibles, leur ordre dans la liste ne détermine pas celui qui est utilisé. Les critères de sélection du format dépendent de facteurs externes tels que la bande passante réseau.
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);
Créez
RawOutgoingVideoStreamOptions
et définissezFormats
avec les objets créés précédemment.RawOutgoingVideoStreamOptions rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions(); rawOutgoingVideoStreamOptions.setFormats(videoStreamFormats);
Créez une instance de
VirtualOutgoingVideoStream
à l’aide de l’instanceRawOutgoingVideoStreamOptions
que vous avez créée précédemment.VirtualOutgoingVideoStream rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
Abonnez-vous au délégué
RawOutgoingVideoStream.addOnFormatChangedListener
. Cet événement informe chaque fois que leVideoStreamFormat
a été modifié à partir de l’un des formats vidéo fournis dans la liste.virtualOutgoingVideoStream.addOnFormatChangedListener((VideoStreamFormatChangedEvent args) -> { VideoStreamFormat videoStreamFormat = args.Format; });
Créez une instance de la classe d’assistance suivante pour générer des données
RawVideoFrame
aléatoires à l’aide 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(); } } }
Abonnez-vous au délégué
VideoStream.addOnStateChangedListener
. Ce délégué indique l’état du flux actuel. N’envoyez pas d’images si l’état n’est pas égal àVideoStreamState.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; } });
Vidéo de partage d’écran
Étant donné que le système Windows génère les images, vous devez implémenter votre propre service de premier plan pour capturer les images et les diffuser à l’aide notre API Azure Communication Services Calling.
Résolutions vidéo prises en charge
Proportions | Résolution | FPS maximum |
---|---|---|
Tout | N’importe quelle valeur jusqu’à 1080p | 30 |
Étapes de la création d’un flux vidéo de partage d’écran
Créez un tableau de
VideoFormat
à l’aide de VideoStreamPixelFormat que le SDK prend en charge.Lorsque plusieurs formats sont disponibles, leur ordre dans la liste ne détermine pas celui qui est utilisé. Les critères de sélection du format dépendent de facteurs externes tels que la bande passante réseau.
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);
Créez
RawOutgoingVideoStreamOptions
et définissezVideoFormats
avec les objets créés précédemment.RawOutgoingVideoStreamOptions rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions(); rawOutgoingVideoStreamOptions.setFormats(videoStreamFormats);
Créez une instance de
VirtualOutgoingVideoStream
à l’aide de l’instanceRawOutgoingVideoStreamOptions
que vous avez créée précédemment.ScreenShareOutgoingVideoStream rawOutgoingVideoStream = new ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions);
Capturez et envoyez l’image vidéo de la manière suivante.
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(); } }
Vidéo entrante brute
Cette fonctionnalité vous permet d’accéder aux images vidéo à l’intérieur des objets IncomingVideoStream
afin de manipuler ces images localement
Créer une instance de
IncomingVideoOptions
qui définitJoinCallOptions
en paramétrantVideoStreamKind.RawIncoming
IncomingVideoOptions incomingVideoOptions = new IncomingVideoOptions() .setStreamType(VideoStreamKind.RAW_INCOMING); JoinCallOptions joinCallOptions = new JoinCallOptions() .setIncomingVideoOptions(incomingVideoOptions);
Une fois que vous recevez un délégué
RemoteParticipant.VideoStreamStateChanged
d’attachement d’événementParticipantsUpdatedEventArgs
. Cet événement informe l’état de l’objetIncomingVideoStream
.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; } }
À ce moment,
IncomingVideoStream
a un déléguéRawIncomingVideoStream.RawVideoFrameReceived
d’attachement d’étatVideoStreamState.Available
, comme indiqué à l’étape précédente. Ce délégué fournit les nouveaux objetsRawVideoFrame
.private void OnVideoFrameReceived(RawVideoFrameReceivedEventArgs args) { // Render/Modify/Save the video frame RawVideoFrameBuffer videoFrame = (RawVideoFrameBuffer) args.getFrame(); }
Dans ce démarrage rapide, vous allez découvrir comment implémenter l’accès aux médias bruts à l’aide du SDK Azure Communication Services Calling pour iOS.
Le SDK Azure Communication Services Calling fournit des API qui permettent aux applications de générer leurs propres images vidéo pour les envoyer ensuite aux participants distants à un appel.
Ce démarrage rapide s’appuie sur le guide Démarrage rapide : Ajouter l’appel vidéo 1 à 1 à votre application pour iOS.
Accès à RawAudio
L’accès à l’audio brut permet d’accéder au flux audio d’un appel entrant et d’afficher et envoyer un flux audio sortant personnalisé lors d’un appel.
Envoyer de l’audio sortant brut
Créez un objet d’options spécifiant les propriétés de flux brut que nous voulons envoyer.
let outgoingAudioStreamOptions = RawOutgoingAudioStreamOptions()
let properties = RawOutgoingAudioStreamProperties()
properties.sampleRate = .hz44100
properties.bufferDuration = .inMs20
properties.channelMode = .mono
properties.format = .pcm16Bit
outgoingAudioStreamOptions.properties = properties
Créez un RawOutgoingAudioStream
et attachez-le aux options d’appel de jointure et le flux démarre automatiquement lorsque l’appel est connecté.
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.
Attacher un flux à un appel
Vous pouvez également attacher le flux à une instance de Call
existante à la place :
call.startAudio(stream: self.rawOutgoingAudioStream) { error in
// Stream attached to `Call`.
}
Commencer à envoyer des échantillons bruts
Nous ne pouvons commencer à envoyer des données qu’une fois que l’état du flux est AudioStreamState.started
.
Pour observer le changement d’état du flux audio, nous implémentons le RawOutgoingAudioStreamDelegate
. Et définissez-le en tant que délégué de flux.
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 en fonction de la fermeture
self.rawOutgoingAudioStream.events.onStateChanged = { args in
// When value is `AudioStreamState.started` we will be able to send audio samples.
}
Lorsque le flux a démarré, nous pouvons commencer à envoyer des échantillons audio AVAudioPCMBuffer
à l’appel.
Le format de mémoire tampon audio doit correspondre aux propriétés de flux spécifiées.
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()
}
}
Il est également important de ne pas oublier d’arrêter le flux audio dans l’instance d’appel Call
actuelle :
call.stopAudio(stream: self.rawOutgoingAudioStream) { error in
// Stream detached from `Call` and stopped.
}
Capture d’échantillons de microphone
À l’aide de AVAudioEngine
d’Apple, nous pouvons capturer des images de microphone en accédant au nœud d’entrée du moteur audio. Et en capturant les données du microphone et en étant en mesure d’utiliser la fonctionnalité audio brute, nous sommes en mesure de traiter l’audio avant de l’envoyer à un appel.
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()
}
}
Notes
Le taux d’échantillonnage du nœud d’entrée du moteur audio est défini par défaut sur une >valeur du taux d’échantillonnage préféré pour la session audio partagée. Nous ne pouvons donc pas installer l’accès à ce nœud à l’aide d’une valeur différente.
Nous devons donc nous assurer que la fréquence d’échantillonnage des propriétés RawOutgoingStream
correspond à celle que nous obtenons en accédant aux échantillons de microphone ou en convertissant les mémoires tampons d’accès au format qui correspond à ce qui est attendu sur le flux sortant.
Avec ce petit exemple, nous avons appris comment capturer les données du microphone AVAudioEngine
et envoyer ces échantillons à un appel à l’aide de la fonctionnalité audio sortante brute.
Recevoir de l’audio entrant brut
Nous pouvons également recevoir les échantillons de flux audio d’appel comme AVAudioPCMBuffer
si nous voulions traiter l’audio avant la lecture.
Créez un objet RawIncomingAudioStreamOptions
spécifiant les propriétés de flux brut que nous voulons recevoir.
let options = RawIncomingAudioStreamOptions()
let properties = RawIncomingAudioStreamProperties()
properties.format = .pcm16Bit
properties.sampleRate = .hz44100
properties.channelMode = .stereo
options.properties = properties
Créer un RawOutgoingAudioStream
et l’attacher aux options d’appel de jointure
let options = JoinCallOptions() // or StartCallOptions()
let incomingAudioOptions = IncomingAudioOptions()
self.rawIncomingStream = RawIncomingAudioStream(rawIncomingAudioStreamOptions: audioStreamOptions)
incomingAudioOptions.stream = self.rawIncomingStream
options.incomingAudioOptions = incomingAudioOptions
Nous pouvons également attacher le flux à une instance de Call
existante à la place :
call.startAudio(stream: self.rawIncomingStream) { error in
// Stream attached to `Call`.
}
Pour commencer à recevoir la mémoire tampon audio brute du flux entrant, implémentez 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.
}
Accès à RawVideo
Étant donné que l’application génère les images vidéo, elle doit indiquer au SDK Azure Communication Services Calling les formats vidéo qu’elle peut générer. Grâce à cette information, le SDK Azure Communication Services Calling peut choisir la meilleure configuration de format vidéo selon les conditions du réseau à ce moment-là.
Vidéo virtuelle
Résolutions vidéo prises en charge
Proportions | Résolution | FPS maximum |
---|---|---|
16x9 | 1080 p | 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 |
Créez un tableau de
VideoFormat
à l’aide de VideoStreamPixelFormat que le SDK prend en charge. Lorsque plusieurs formats sont disponibles, leur ordre dans la liste ne détermine pas celui qui est utilisé. Les critères de sélection du format dépendent de facteurs externes tels que la bande passante réseau.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)
Créez
RawOutgoingVideoStreamOptions
et définissez les formats avec les objets créés précédemment.var rawOutgoingVideoStreamOptions = RawOutgoingVideoStreamOptions() rawOutgoingVideoStreamOptions.formats = videoStreamFormats
Créez une instance de
VirtualOutgoingVideoStream
à l’aide de l’instanceRawOutgoingVideoStreamOptions
que vous avez créée précédemment.var rawOutgoingVideoStream = VirtualOutgoingVideoStream(videoStreamOptions: rawOutgoingVideoStreamOptions)
Implémentez sur le délégué
VirtualOutgoingVideoStreamDelegate
. L’événementdidChangeFormat
informe chaque fois que leVideoStreamFormat
a été modifié à partir de l’un des formats vidéo fournis dans la liste.virtualOutgoingVideoStream.delegate = /* Attach delegate and implement didChangeFormat */
Créer une instance de la classe d’assistance suivante pour accéder aux données 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 } }
Créez une instance de la classe d’assistance suivante pour générer des données aléatoires de
RawVideoFrameBuffer
à l’aide 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 } } }
Implémentez sur le
VirtualOutgoingVideoStreamDelegate
. L’événementdidChangeState
informe l’état du flux actuel. N’envoyez pas d’images si l’état n’est pas égal àVideoStreamState.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 } }
Vidéo de partage d’écran
Étant donné que le système Windows génère les images, vous devez implémenter votre propre service de premier plan pour capturer les images et les diffuser à l’aide notre API Azure Communication Services Calling.
Résolutions vidéo prises en charge
Proportions | Résolution | FPS maximum |
---|---|---|
Tout | N’importe quelle valeur jusqu’à 1080p | 30 |
Étapes de la création d’un flux vidéo de partage d’écran
Créez un tableau de
VideoFormat
à l’aide de VideoStreamPixelFormat que le SDK prend en charge. Lorsque plusieurs formats sont disponibles, leur ordre dans la liste ne détermine pas celui qui est utilisé. Les critères de sélection du format dépendent de facteurs externes tels que la bande passante réseau.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)
Créez
RawOutgoingVideoStreamOptions
et définissezVideoFormats
avec les objets créés précédemment.var rawOutgoingVideoStreamOptions = RawOutgoingVideoStreamOptions() rawOutgoingVideoStreamOptions.formats = videoStreamFormats
Créez une instance de
VirtualOutgoingVideoStream
à l’aide de l’instanceRawOutgoingVideoStreamOptions
que vous avez créée précédemment.var rawOutgoingVideoStream = ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions)
Capturez et envoyez l’image vidéo de la manière suivante.
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*/ } }
Vidéo entrante brute
Cette fonctionnalité vous permet d’accéder aux images vidéo à l’intérieur de l’objet IncomingVideoStream
pour manipuler ces objets de flux localement
Créer une instance de
IncomingVideoOptions
qui définitJoinCallOptions
en paramétrantVideoStreamKind.RawIncoming
var incomingVideoOptions = IncomingVideoOptions() incomingVideoOptions.streamType = VideoStreamKind.rawIncoming var joinCallOptions = JoinCallOptions() joinCallOptions.incomingVideoOptions = incomingVideoOptions
Une fois que vous recevez un délégué
RemoteParticipant.delegate.didChangedVideoStreamState
d’attachement d’événementParticipantsUpdatedEventArgs
. Cet événement informe l’état des objetsIncomingVideoStream
.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 } }
À ce moment,
IncomingVideoStream
a un déléguéRawIncomingVideoStream.delegate.didReceivedRawVideoFrame
d’attachement d’étatVideoStreamState.available
, comme indiqué à l’étape précédente. Cet événement fournit les nouveaux objetsRawVideoFrame
.func rawIncomingVideoStream(_ rawIncomingVideoStream: RawIncomingVideoStream, didRawVideoFrameReceived args: RawVideoFrameReceivedEventArgs) { /* Render/Modify/Save the video frame */ let videoFrame = args.frame as! RawVideoFrameBuffer }
En tant que développeur, vous pouvez accéder aux médias bruts des contenus audio, vidéo et de partage d’écran entrants et sortants pendant un appel afin de pouvoir capturer, analyser et traiter le contenu audio/vidéo. L’accès à l’audio et à la vidéo brutes ainsi qu’au partage d’écran bruts côté client d’Azure Communication Services permet aux développeurs d’afficher et de modifier de manière presque illimitée le contenu audio, vidéo et de partage d’écran qui se produit dans le SDK d’appel d'Azure Communication Services. Dans ce démarrage rapide, vous allez découvrir comment implémenter l’accès aux médias bruts à l’aide du SDK Azure Communication Services Calling pour JavaScript.
Par exemple,
- Vous pouvez accéder au flux audio/vidéo de l’appel directement sur l’objet de l’appel et transmettre des flux audio/vidéo sortants personnalisés pendant l’appel.
- Vous pouvez inspecter des flux audio et vidéo pour exécuter des modèles d’IA personnalisés à des fins d’analyse. Ces modèles peuvent inclure le traitement du langage naturel pour analyser les conversations ou fournir des insights et des suggestions en temps réel afin d’augmenter la productivité des agents.
- Les organisations peuvent exploiter les flux audio et vidéo à des fins d’analyse des sentiments des patients en téléconsultation ou d’assistance à distance lors d’appels vidéo en réalité mixte. Grâce à cette fonctionnalité, les développeurs peuvent également mettre l’innovation au service de l’amélioration des expériences interactives.
Prérequis
Important
Les exemples illustrés ici sont disponibles dans la version 1.13.1 de l’appel du kit de développement logiciel (SDK) pour JavaScript. Assurez-vous d’utiliser cette version ou une version ultérieure lorsque vous effectuez ce démarrage rapide.
Accéder à l’audio brut
L’accès à l’audio brut permet d’accéder au flux audio d’un appel entrant et d’afficher et envoyer un flux audio sortant personnalisé lors d’un appel.
Accéder à un flux audio brut entrant
Utilisez le code suivant pour accéder au flux audio d’un appel entrant.
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);
Passer un appel avec un flux audio personnalisé
Utilisez le code suivant pour démarrer un appel avec un flux audio personnalisé au lieu du microphone de l’utilisateur.
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);
Basculer vers un flux audio personnalisé au cours d’un appel
Utilisez le code suivant pour que l’appareil d’entrée bascule du microphone de l’utilisateur vers un flux audio personnalisé au cours d’un appel.
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);
Interrompre le flux audio personnalisé
Utilisez le code suivant pour interrompre l’envoi d’un flux audio personnalisé déjà défini au cours d’un appel.
call.stopAudio();
Accéder aux vidéos brutes
Les vidéos brutes vous indiquent l’instance d’un objet MediaStream
. (Pour en savoir plus, consultez la documentation JavaScript.) Les vidéos brutes donnent accès spécifiquement à l’objet MediaStream
des appels entrants et sortants. Pour les vidéos brutes, vous pouvez utiliser cet objet afin d’appliquer des filtres de traitement des images vidéo à l’aide du Machine Learning.
Vous pouvez envoyer les images vidéo brutes sortantes traitées en tant que vidéos sortantes de l’émetteur. Les images vidéo brutes entrantes traitées peuvent être affichées côté récepteur.
Passer un appel avec un flux vidéo personnalisé
Vous pouvez accéder au flux vidéo brut d’un appel entrant. Vous utilisez MediaStream
pour le flux vidéo brut sortant afin de traiter les images à l’aide du Machine Learning et d’appliquer des filtres. La vidéo sortante traitée peut ensuite être envoyée en tant que flux vidéo de l’émetteur.
Cet exemple illustre l’envoi de données de canevas à un utilisateur sous la forme d’une vidéo sortante.
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);
Basculer vers un flux vidéo personnalisé au cours d’un appel
Utilisez le code suivant pour que l’appareil d’entrée bascule de la caméra de l’utilisateur vers un flux vidéo personnalisé au cours d’un appel.
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);
Interrompre le flux vidéo personnalisé
Utilisez le code suivant pour interrompre l’envoi d’un flux vidéo personnalisé déjà défini au cours d’un appel.
// Stop video by passing the same `localVideoStream` instance that was used to start video
await call.stopVideo(localVideoStream);
Lorsque vous passez d’une caméra ayant appliqué des effets personnalisés à un autre appareil photo, arrêtez d’abord la vidéo, modifiez la source sur l’appareil LocalVideoStream
, puis relancez la vidéo.
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);
}
Accéder au flux vidéo entrant d’un participant distant
Vous pouvez accéder au flux vidéo brut pour un appel entrant. Vous utilisez MediaStream
pour le flux vidéo brut entrant afin de traiter les images à l’aide du Machine Learning et d’appliquer des filtres. La vidéo entrante traitée peut ensuite être affichée côté récepteur.
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();
Important
Cette fonctionnalité d’Azure Communication Services est actuellement en préversion.
Ces interfaces de programmation d’applications et kits de développement logiciel (SDK) en préversion sont fournis sans contrat au niveau du service. Nous vous recommandons de ne pas les utiliser pour les charges de travail de production. Certaines fonctionnalités peuvent être limitées ou non prises en charge.
Pour plus d’informations, consultez Conditions d’utilisation supplémentaires relatives aux préversions de Microsoft Azure.
L’accès au partage d’écran brut est en préversion publique et disponible dans le cadre de la version 1.15.1-beta.1+.
Accéder au partage d’écran brut
Le média de partage d’écran brut permet d’accéder spécifiquement à l’objet MediaStream
pour les flux de partage d’écran entrants et sortants. Pour le partage d’écran brut, vous pouvez utiliser cet objet afin d’appliquer des filtres à l’aide de l’apprentissage automatique pour traiter les images du partage d’écran.
Les images brutes de partage d’écran traitées peuvent être envoyées en tant que partage d’écran sortant de l’expéditeur. Les images brutes de partage d’écran entrantes traitées peuvent être affichées du côté du récepteur.
Remarque : l’envoi d’un partage d’écran n’est pris en charge que sur le navigateur de bureau.
Lancez le partage d’écran avec un flux de partage d’écran personnalisé
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'
Accédez au flux de partage d’écran brut à partir d’un écran, d’un onglet de navigateur ou d’une application, et appliquez des effets au flux
Voici un exemple d’application d’un effet noir et blanc sur le flux de partage d’écran brut à partir d’un écran, d’un onglet de navigateur ou d’une application. REMARQUE : l’API de filtre de contexte Canvas = "grayscale(1)" n’est pas prise en charge dans 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;
Interrompez l’envoi de flux de partage d’écran
Utilisez le code suivant pour interrompre l’envoi d’un flux de partage d’écran personnalisé après qu’il a été défini au cours d’un appel.
// Stop sending raw screen sharing stream
await call.stopScreenSharing(localScreenSharingStream);
Accédez au flux de partage d’écran entrant à partir d’un participant distant
Vous pouvez accéder au flux de partage d’écran brut à partir d’un participant distant. Vous utilisez MediaStream
pour le flux de partage d’écran brut entrant afin de traiter les images à l’aide de l’apprentissage automatique et d’appliquer des filtres. Le flux de partage d’écran entrant traité peut ensuite être affiché côté récepteur.
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();
Étapes suivantes
Pour plus d’informations, consultez les articles suivants :
- Consultez l’exemple d’appel.
- Familiarisez-vous avec la bibliothèque d’interface utilisateur.
- Découvrez les fonctionnalités du SDK Calling.
- Apprenez-en davantage sur le fonctionnement des appels.