クイックスタート: アプリに未加工メディア アクセスを追加する
このクイックスタートでは、Unity 用 Azure Communication Services Calling SDK を使用して、未加工メディア アクセスを実装する方法について説明します。 Azure Communication Services Calling SDK の API をアプリで使うと、通話で独自のビデオ フレームを生成して送信したり、リモート参加者からの未加工ビデオ フレームをレンダリングしたりできます。 このクイックスタートは、Unity 用の「クイックスタート: アプリに 1 対 1 のビデオ通話を追加する」が基になっています。
RawVideo アクセス
ビデオ フレームを生成するため、アプリで生成できるビデオ形式についてアプリから Azure Communication Services Calling SDK に通知する必要があります。 この情報によって、所定の時間に所定のネットワーク条件で最も適切なビデオ形式構成を Azure Communication Services Calling SDK で選択できます。
仮想ビデオ
サポートされているビデオ解像度
[縦横比] | 解像度 | 最大 FPS |
---|---|---|
16 x 9 | 1080p | 30 |
16 x 9 | 720p | 30 |
16 x 9 | 540p | 30 |
16 x 9 | 480p | 30 |
16 x 9 | 360p | 30 |
16 x 9 | 270p | 15 |
16 x 9 | 240p | 15 |
16 x 9 | 180p | 15 |
4 x 3 | VGA (640 x 480) | 30 |
4 x 3 | 424 x 320 | 15 |
4 x 3 | QVGA (320 x 240) | 15 |
4 x 3 | 212 x 160 | 15 |
「クイックスタート: アプリに 1 対 1 のビデオ通話を追加する」の手順に従って、Unity ゲームを作成します。 目的は、通話を始められる状態の
CallAgent
オブジェクトを取得することです。 このクイックスタートの最終的なコードは GitHub にあります。SDK でサポートされている VideoStreamPixelFormat を使用して
VideoFormat
の配列を作成します。 使用できる形式が複数ある場合、リスト内の形式の順序は、どの形式が使用されるかに影響しません。つまり、この順序で形式の優先順位は決まりません。 形式の選択の条件は、ネットワーク帯域幅などの外部要因に基づきます。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 };
RawOutgoingVideoStreamOptions
を作成し、前に作成したオブジェクトでFormats
を設定します。var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions { Formats = videoStreamFormats };
前に作成した
RawOutgoingVideoStreamOptions
インスタンスを使用して、VirtualOutgoingVideoStream
のインスタンスを作成します。var rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
RawOutgoingVideoStream.FormatChanged
デリゲートをサブスクライブします。 このイベントは、リストで提供されているビデオ形式のいずれかからVideoStreamFormat
が変更されるたびに通知します。rawOutgoingVideoStream.FormatChanged += (object sender, VideoStreamFormatChangedEventArgs args) { VideoStreamFormat videoStreamFormat = args.Format; }
RawOutgoingVideoStream.StateChanged
デリゲートをサブスクライブします。 このイベントは、State
が変化するたびに通知します。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; } }
開始や停止などの未加工送信ビデオ ストリームの状態トランザクションを処理し、カスタム ビデオ フレームの生成を開始するか、フレーム生成アルゴリズムを一時停止します。
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; } }
次に示すのは、送信ビデオ フレーム ジェネレーターの例です。
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 }; }
Note
このメソッドで
unsafe
修飾子が使われているのは、NativeBuffer
でネイティブ メモリ リソースにアクセスする必要があるためです。 そのため、Unity エディターでAllow unsafe
オプションを有効にする必要もあります。同様に、ビデオ ストリームの
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 }); }
MonoBehaviour.Update()
コールバック メソッドをオーバーロードしなくてよいように、バッファリング メカニズムを介して着信と発信の両方のビデオ フレームを管理することを強くお勧めします。このメソッドは軽量なままにし、CPU やネットワークに大きな負荷がかからないようにして、スムーズなビデオ エクスペリエンスを保証する必要があります。 この最適化を行うかどうかは、開発者によるシナリオで最適に動作するものの判断に委ねられます。内部キューから
Graphics.Blit
を呼び出すことによって、Unity のVideoTexture
に着信フレームをレンダリングする方法の例を次に示します。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(); } }
このクイックスタートでは、Windows 用 Azure Communication Services Calling SDK を使用して、未加工メディア アクセスを実装する方法について説明します。 Azure Communication Services Calling SDK が提供する API を使用すると、アプリが通話でリモート参加者に送信する独自のビデオ フレームを生成できます。 このクイックスタートは、Windows 用の「クイックスタート: アプリに 1 対 1 のビデオ通話を追加する」に基づいています。
RawAudio アクセス
未加工オーディオ メディアにアクセスすると、着信オーディオ ストリームにアクセスできると共に、通話中にカスタム発信オーディオ ストリームを表示および送信できます。
未加工の発信オーディオを送信する
送信する未加工ストリームのプロパティを指定する options オブジェクトを作成します。
RawOutgoingAudioStreamProperties outgoingAudioProperties = new RawOutgoingAudioStreamProperties()
{
Format = ACSAudioStreamFormat.Pcm16Bit,
SampleRate = AudioStreamSampleRate.Hz48000,
ChannelMode = AudioStreamChannelMode.Stereo,
BufferDuration = AudioStreamBufferDuration.InMs20
};
RawOutgoingAudioStreamOptions outgoingAudioStreamOptions = new RawOutgoingAudioStreamOptions()
{
Properties = outgoingAudioProperties
};
RawOutgoingAudioStream
を作成して通話参加オプションにアタッチすると、通話が接続されたときにストリームが自動的に開始されます。
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.
通話にストリームをアタッチする
または、ストリームを既存の Call
インスタンスにアタッチすることもできます。
await call.StartAudio(rawOutgoingAudioStream);
未加工サンプルの送信を開始する
データの送信を開始できるのは、ストリームの状態が AudioStreamState.Started
になった場合のみです。
オーディオ ストリームの状態の変化を確認するには、OnStateChangedListener
イベントにリスナーを追加します。
unsafe private void AudioStateChanged(object sender, AudioStreamStateChanged args)
{
if (args.AudioStreamState == AudioStreamState.Started)
{
// We can now start sending samples.
}
}
outgoingAudioStream.StateChanged += AudioStateChanged;
ストリームが開始されたら、通話への MemoryBuffer
オーディオ サンプルの送信を開始できます。
オーディオ バッファー形式は、指定したストリーム プロパティと一致している必要があります。
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();
}
未加工の着信オーディオを受信する
再生前に通話オーディオ ストリームを処理する場合は、MemoryBuffer
として通話オーディオ ストリームのサンプルを受信することもできます。
受信したい未加工ストリーム プロパティを指定して RawIncomingAudioStreamOptions
オブジェクトを作成します。
RawIncomingAudioStreamProperties properties = new RawIncomingAudioStreamProperties()
{
Format = AudioStreamFormat.Pcm16Bit,
SampleRate = AudioStreamSampleRate.Hz44100,
ChannelMode = AudioStreamChannelMode.Stereo
};
RawIncomingAudioStreamOptions options = new RawIncomingAudioStreamOptions()
{
Properties = properties
};
RawIncomingAudioStream
を作成し、通話参加オプションにアタッチします。
JoinCallOptions options = JoinCallOptions(); // or StartCallOptions()
RawIncomingAudioStream rawIncomingAudioStream = new RawIncomingAudioStream(audioStreamOptions);
IncomingAudioOptions incomingAudioOptions = new IncomingAudioOptions()
{
Stream = rawIncomingAudioStream
};
options.IncomingAudioOptions = incomingAudioOptions;
または、ストリームを既存の Call
インスタンスにアタッチすることもできます。
await call.startAudio(context, rawIncomingAudioStream);
着信ストリームからの未加工オーディオ バッファーの受信を開始するために、着信ストリームの状態にリスナーを追加し、受信イベントをバッファーに格納します。
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;
RawVideo アクセス
ビデオ フレームを生成するため、アプリで生成できるビデオ形式についてアプリから Azure Communication Services Calling SDK に通知する必要があります。 この情報によって、所定の時間に所定のネットワーク条件で最も適切なビデオ形式構成を Azure Communication Services Calling SDK で選択できます。
仮想ビデオ
サポートされているビデオ解像度
[縦横比] | 解像度 | 最大 FPS |
---|---|---|
16 x 9 | 1080p | 30 |
16 x 9 | 720p | 30 |
16 x 9 | 540p | 30 |
16 x 9 | 480p | 30 |
16 x 9 | 360p | 30 |
16 x 9 | 270p | 15 |
16 x 9 | 240p | 15 |
16 x 9 | 180p | 15 |
4 x 3 | VGA (640 x 480) | 30 |
4 x 3 | 424 x 320 | 15 |
4 x 3 | QVGA (320 x 240) | 15 |
4 x 3 | 212 x 160 | 15 |
SDK でサポートされている VideoStreamPixelFormat を使用して
VideoFormat
の配列を作成します。 使用できる形式が複数ある場合、リスト内の形式の順序は、どの形式が使用されるかに影響しません。つまり、この順序で形式の優先順位は決まりません。 形式の選択の条件は、ネットワーク帯域幅などの外部要因に基づきます。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 };
RawOutgoingVideoStreamOptions
を作成し、前に作成したオブジェクトでFormats
を設定します。var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions { Formats = videoStreamFormats };
前に作成した
RawOutgoingVideoStreamOptions
インスタンスを使用して、VirtualOutgoingVideoStream
のインスタンスを作成します。var rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
RawOutgoingVideoStream.FormatChanged
デリゲートをサブスクライブします。 このイベントは、リストで提供されているビデオ形式のいずれかからVideoStreamFormat
が変更されるたびに通知します。rawOutgoingVideoStream.FormatChanged += (object sender, VideoStreamFormatChangedEventArgs args) { VideoStreamFormat videoStreamFormat = args.Format; }
バッファー データにアクセスするために、次のヘルパー クラスのインスタンスを作成します
[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; } }
VideoStreamPixelFormat.Rgba
を使用してランダムなRawVideoFrame
を生成するために、次のヘルパー クラスのインスタンスを作成します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; } } }
VideoStream.StateChanged
デリゲートをサブスクライブします。 このイベントは、現在のストリームの状態を通知します。 状態が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; } };
画面共有ビデオ
Windows システムではフレームが生成されるため、独自のフォアグラウンド サービスを実装してフレームをキャプチャし、Azure Communication Services Calling API を使用して送信する必要があります。
サポートされているビデオ解像度
[縦横比] | 解像度 | 最大 FPS |
---|---|---|
すべて | 任意 (最大 1080p) | 30 |
画面共有ビデオ ストリームを作成する手順
- SDK でサポートされている VideoStreamPixelFormat を使用して
VideoFormat
の配列を作成します。 使用できる形式が複数ある場合、リスト内の形式の順序は、どの形式が使用されるかに影響しません。つまり、この順序で形式の優先順位は決まりません。 形式の選択の条件は、ネットワーク帯域幅などの外部要因に基づきます。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 };
RawOutgoingVideoStreamOptions
を作成し、前に作成したオブジェクトでVideoFormats
を設定します。var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions { Formats = videoStreamFormats };
- 前に作成した
RawOutgoingVideoStreamOptions
インスタンスを使用して、VirtualOutgoingVideoStream
のインスタンスを作成します。var rawOutgoingVideoStream = new ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions);
- 次の方法でビデオ フレームをキャプチャして送信します。
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; } }
未加工の着信ビデオ
この機能を使用すると、IncomingVideoStream
内のビデオ フレームにアクセスして、それらのストリームをローカルで操作できます
VideoStreamKind.RawIncoming
を設定したJoinCallOptions
によって設定されるIncomingVideoOptions
のインスタンスを作成します。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 };
ParticipantsUpdatedEventArgs
イベントを受信したら、RemoteParticipant.VideoStreamStateChanged
デリゲートをアタッチします。 このイベントは、IncomingVideoStream
オブジェクトの状態を通知します。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; } }
- この時点では、
IncomingVideoStream
の状態はVideoStreamState.Available
です。前の手順で示したように、RawIncomingVideoStream.RawVideoFrameReceived
デリゲートをアタッチします。 これにより、新しいRawVideoFrame
オブジェクトが提供されます。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; } }
このクイックスタートでは、Android 用 Azure Communication Services Calling SDK を使用して、未加工メディア アクセスを実装する方法について説明します。
Azure Communication Services Calling SDK が提供する API を使用すると、アプリが通話でリモート参加者に送信する独自のビデオ フレームを生成できます。
このクイックスタートは、Android 用の「クイックスタート: アプリに 1 対 1 のビデオ通話を追加する」に基づいています。
RawAudio アクセス
未加工オーディオ メディアにアクセスすると、通話の着信オーディオ ストリームにアクセスできると共に、通話中にカスタム発信オーディオ ストリームを表示および送信できます。
未加工の発信オーディオを送信する
送信する未加工ストリームのプロパティを指定する options オブジェクトを作成します。
RawOutgoingAudioStreamProperties outgoingAudioProperties = new RawOutgoingAudioStreamProperties()
.setAudioFormat(AudioStreamFormat.PCM16_BIT)
.setSampleRate(AudioStreamSampleRate.HZ44100)
.setChannelMode(AudioStreamChannelMode.STEREO)
.setBufferDuration(AudioStreamBufferDuration.IN_MS20);
RawOutgoingAudioStreamOptions outgoingAudioStreamOptions = new RawOutgoingAudioStreamOptions()
.setProperties(outgoingAudioProperties);
RawOutgoingAudioStream
を作成して通話参加オプションにアタッチすると、通話が接続されたときにストリームが自動的に開始されます。
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.
通話にストリームをアタッチする
または、ストリームを既存の Call
インスタンスにアタッチすることもできます。
CompletableFuture<Void> result = call.startAudio(context, rawOutgoingAudioStream);
未加工サンプルの送信を開始する
データの送信を開始できるのは、ストリームの状態が AudioStreamState.STARTED
になった場合のみです。
オーディオ ストリームの状態の変化を確認するには、OnStateChangedListener
イベントにリスナーを追加します。
private void onStateChanged(PropertyChangedEvent propertyChangedEvent) {
// When value is `AudioStreamState.STARTED` we'll be able to send audio samples.
}
rawOutgoingAudioStream.addOnStateChangedListener(this::onStateChanged)
ストリームが開始されたら、通話への java.nio.ByteBuffer
オーディオ サンプルの送信を開始できます。
オーディオ バッファー形式は、指定したストリーム プロパティと一致している必要があります。
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();
未加工の着信オーディオを受信する
再生前にオーディオを処理する場合は、java.nio.ByteBuffer
として通話オーディオ ストリームのサンプルを受信することもできます。
受信したい未加工ストリーム プロパティを指定して RawIncomingAudioStreamOptions
オブジェクトを作成します。
RawIncomingAudioStreamOptions options = new RawIncomingAudioStreamOptions();
RawIncomingAudioStreamProperties properties = new RawIncomingAudioStreamProperties()
.setAudioFormat(AudioStreamFormat.PCM16_BIT)
.setSampleRate(AudioStreamSampleRate.HZ44100)
.setChannelMode(AudioStreamChannelMode.STEREO);
options.setProperties(properties);
RawIncomingAudioStream
を作成し、通話参加オプションにアタッチします。
JoinCallOptions options = JoinCallOptions() // or StartCallOptions()
IncomingAudioOptions incomingAudioOptions = new IncomingAudioOptions();
RawIncomingAudioStream rawIncomingAudioStream = new RawIncomingAudioStream(audioStreamOptions);
incomingAudioOptions.setStream(rawIncomingAudioStream);
options.setIncomingAudioOptions(incomingAudioOptions);
または、ストリームを既存の Call
インスタンスにアタッチすることもできます。
CompletableFuture<Void> result = call.startAudio(context, rawIncomingAudioStream);
着信ストリームからの未加工オーディオ バッファーの受信を開始するために、着信ストリームの状態にリスナーを追加し、受信イベントをバッファーに格納します。
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);
また、現在の通話の Call
インスタンスでオーディオ ストリームを忘れずに停止することも重要です。
CompletableFuture<Void> result = call.stopAudio(context, rawIncomingAudioStream);
RawVideo アクセス
ビデオ フレームを生成するため、アプリで生成できるビデオ形式についてアプリから Azure Communication Services Calling SDK に通知する必要があります。 この情報によって、所定の時間に所定のネットワーク条件で最も適切なビデオ形式構成を Azure Communication Services Calling SDK で選択できます。
仮想ビデオ
サポートされているビデオ解像度
[縦横比] | 解像度 | 最大 FPS |
---|---|---|
16 x 9 | 1080p | 30 |
16 x 9 | 720p | 30 |
16 x 9 | 540p | 30 |
16 x 9 | 480p | 30 |
16 x 9 | 360p | 30 |
16 x 9 | 270p | 15 |
16 x 9 | 240p | 15 |
16 x 9 | 180p | 15 |
4 x 3 | VGA (640 x 480) | 30 |
4 x 3 | 424 x 320 | 15 |
4 x 3 | QVGA (320 x 240) | 15 |
4 x 3 | 212 x 160 | 15 |
SDK でサポートされている VideoStreamPixelFormat を使用して
VideoFormat
の配列を作成します。使用できる形式が複数ある場合、リスト内の形式の順序は、どの形式が使用されるかに影響しません。つまり、この順序で形式の優先順位は決まりません。 形式の選択の条件は、ネットワーク帯域幅などの外部要因に基づきます。
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);
RawOutgoingVideoStreamOptions
を作成し、前に作成したオブジェクトでFormats
を設定します。RawOutgoingVideoStreamOptions rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions(); rawOutgoingVideoStreamOptions.setFormats(videoStreamFormats);
前に作成した
RawOutgoingVideoStreamOptions
インスタンスを使用して、VirtualOutgoingVideoStream
のインスタンスを作成します。VirtualOutgoingVideoStream rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
RawOutgoingVideoStream.addOnFormatChangedListener
デリゲートをサブスクライブします。 このイベントは、リストで提供されているビデオ形式のいずれかからVideoStreamFormat
が変更されるたびに通知します。virtualOutgoingVideoStream.addOnFormatChangedListener((VideoStreamFormatChangedEvent args) -> { VideoStreamFormat videoStreamFormat = args.Format; });
VideoStreamPixelFormat.RGBA
を使用してランダムなRawVideoFrame
を生成するために、次のヘルパー クラスのインスタンスを作成します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(); } } }
VideoStream.addOnStateChangedListener
デリゲートをサブスクライブします。 このデリゲートは現在のストリームの状態を通知します。 状態が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; } });
ScreenShare ビデオ
Windows システムではフレームが生成されるため、独自のフォアグラウンド サービスを実装してフレームをキャプチャし、Azure Communication Services Calling API を使用して送信する必要があります。
サポートされているビデオ解像度
[縦横比] | 解像度 | 最大 FPS |
---|---|---|
すべて | 任意 (最大 1080p) | 30 |
画面共有ビデオ ストリームを作成する手順
SDK でサポートされている VideoStreamPixelFormat を使用して
VideoFormat
の配列を作成します。使用できる形式が複数ある場合、リスト内の形式の順序は、どの形式が使用されるかに影響しません。つまり、この順序で形式の優先順位は決まりません。 形式の選択の条件は、ネットワーク帯域幅などの外部要因に基づきます。
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);
RawOutgoingVideoStreamOptions
を作成し、前に作成したオブジェクトでVideoFormats
を設定します。RawOutgoingVideoStreamOptions rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions(); rawOutgoingVideoStreamOptions.setFormats(videoStreamFormats);
前に作成した
RawOutgoingVideoStreamOptions
インスタンスを使用して、VirtualOutgoingVideoStream
のインスタンスを作成します。ScreenShareOutgoingVideoStream rawOutgoingVideoStream = new ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions);
次の方法でビデオ フレームをキャプチャして送信します。
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(); } }
未加工の着信ビデオ
この機能を使用すると、IncomingVideoStream
オブジェクト内のビデオ フレームをローカルで操作するために、それらのフレームにアクセスできます
VideoStreamKind.RawIncoming
を設定したJoinCallOptions
によって設定されるIncomingVideoOptions
のインスタンスを作成します。IncomingVideoOptions incomingVideoOptions = new IncomingVideoOptions() .setStreamType(VideoStreamKind.RAW_INCOMING); JoinCallOptions joinCallOptions = new JoinCallOptions() .setIncomingVideoOptions(incomingVideoOptions);
ParticipantsUpdatedEventArgs
イベントを受信したら、RemoteParticipant.VideoStreamStateChanged
デリゲートをアタッチします。 このイベントは、IncomingVideoStream
オブジェクトの状態を通知します。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; } }
この時点では、
IncomingVideoStream
の状態はVideoStreamState.Available
です。前の手順で示したように、RawIncomingVideoStream.RawVideoFrameReceived
デリゲートをアタッチします。 このデリゲートにより、新しいRawVideoFrame
オブジェクトが提供されます。private void OnVideoFrameReceived(RawVideoFrameReceivedEventArgs args) { // Render/Modify/Save the video frame RawVideoFrameBuffer videoFrame = (RawVideoFrameBuffer) args.getFrame(); }
このクイックスタートでは、iOS 用 Azure Communication Services Calling SDK を使用して、未加工メディア アクセスを実装する方法について説明します。
Azure Communication Services Calling SDK が提供する API を使用すると、アプリが通話でリモート参加者に送信する独自のビデオ フレームを生成できます。
このクイックスタートは、iOS 用の「クイックスタート: アプリに 1 対 1 のビデオ通話を追加する」に基づいています。
RawAudio アクセス
未加工オーディオ メディアにアクセスすると、着信オーディオ ストリームにアクセスできると共に、通話中にカスタム発信オーディオ ストリームを表示および送信できます。
未加工の発信オーディオを送信する
送信する未加工ストリームのプロパティを指定する options オブジェクトを作成します。
let outgoingAudioStreamOptions = RawOutgoingAudioStreamOptions()
let properties = RawOutgoingAudioStreamProperties()
properties.sampleRate = .hz44100
properties.bufferDuration = .inMs20
properties.channelMode = .mono
properties.format = .pcm16Bit
outgoingAudioStreamOptions.properties = properties
RawOutgoingAudioStream
を作成して通話参加オプションにアタッチすると、通話が接続されたときにストリームが自動的に開始されます。
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.
通話にストリームをアタッチする
または、ストリームを既存の Call
インスタンスにアタッチすることもできます。
call.startAudio(stream: self.rawOutgoingAudioStream) { error in
// Stream attached to `Call`.
}
未加工サンプルの送信を開始する
データの送信を開始できるのは、ストリームの状態が AudioStreamState.started
になった場合のみです。
オーディオ ストリームの状態の変化を確認するために、RawOutgoingAudioStreamDelegate
を実装します。 これをストリーム デリゲートとして設定します。
func rawOutgoingAudioStream(_ rawOutgoingAudioStream: RawOutgoingAudioStream,
didChangeState args: AudioStreamStateChangedEventArgs) {
// When value is `AudioStreamState.started` we will be able to send audio samples.
}
self.rawOutgoingAudioStream.delegate = DelegateImplementer()
またはクロージャ ベースを使用します
self.rawOutgoingAudioStream.events.onStateChanged = { args in
// When value is `AudioStreamState.started` we will be able to send audio samples.
}
ストリームが開始されたら、通話への AVAudioPCMBuffer
オーディオ サンプルの送信を開始できます。
オーディオ バッファー形式は、指定したストリーム プロパティと一致している必要があります。
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()
}
}
また、現在の通話の Call
インスタンスでオーディオ ストリームを忘れずに停止することも重要です。
call.stopAudio(stream: self.rawOutgoingAudioStream) { error in
// Stream detached from `Call` and stopped.
}
マイクのサンプルのキャプチャ
Apple の AVAudioEngine
を使用すると、オーディオ エンジンの入力ノードにタップをインストールして、マイク フレームをキャプチャできます。 マイク データのキャプチャと未加工オーディオ機能の利用により、通話に送信する前にオーディオを処理できます。
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()
}
}
Note
オーディオ エンジンの入力ノードは、共有オーディオ セッションの推奨サンプル レートの値に既定で設定されます。 したがって、別の値を使用してそのノードにタップをインストールすることはできません。
そのため、RawOutgoingStream
プロパティのサンプル レートが、タップから取得してマイク サンプルに送信したものと一致することを確認するか、発信ストリームで想定される形式にタップ バッファーを変換する必要があります。
この小規模なサンプルでは、マイクの AVAudioEngine
データをキャプチャし、未加工の発信オーディオ機能を使用してそれらのサンプルを通話に送信する方法を学習しました。
未加工の着信オーディオを受信する
再生前にオーディオを処理する場合は、AVAudioPCMBuffer
として通話オーディオ ストリームのサンプルを受信することもできます。
受信したい未加工ストリーム プロパティを指定して RawIncomingAudioStreamOptions
オブジェクトを作成します。
let options = RawIncomingAudioStreamOptions()
let properties = RawIncomingAudioStreamProperties()
properties.format = .pcm16Bit
properties.sampleRate = .hz44100
properties.channelMode = .stereo
options.properties = properties
RawOutgoingAudioStream
を作成し、通話参加オプションにアタッチします。
let options = JoinCallOptions() // or StartCallOptions()
let incomingAudioOptions = IncomingAudioOptions()
self.rawIncomingStream = RawIncomingAudioStream(rawIncomingAudioStreamOptions: audioStreamOptions)
incomingAudioOptions.stream = self.rawIncomingStream
options.incomingAudioOptions = incomingAudioOptions
または、ストリームを既存の Call
インスタンスにアタッチすることもできます。
call.startAudio(stream: self.rawIncomingStream) { error in
// Stream attached to `Call`.
}
着信ストリームから未加工オーディオ バッファーの受信を開始するために、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()
または
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.
}
RawVideo アクセス
ビデオ フレームを生成するため、アプリで生成できるビデオ形式についてアプリから Azure Communication Services Calling SDK に通知する必要があります。 この情報によって、所定の時間に所定のネットワーク条件で最も適切なビデオ形式構成を Azure Communication Services Calling SDK で選択できます。
仮想ビデオ
サポートされているビデオ解像度
[縦横比] | 解像度 | 最大 FPS |
---|---|---|
16 x 9 | 1080p | 30 |
16 x 9 | 720p | 30 |
16 x 9 | 540p | 30 |
16 x 9 | 480p | 30 |
16 x 9 | 360p | 30 |
16 x 9 | 270p | 15 |
16 x 9 | 240p | 15 |
16 x 9 | 180p | 15 |
4 x 3 | VGA (640 x 480) | 30 |
4 x 3 | 424 x 320 | 15 |
4 x 3 | QVGA (320 x 240) | 15 |
4 x 3 | 212 x 160 | 15 |
SDK でサポートされている VideoStreamPixelFormat を使用して
VideoFormat
の配列を作成します。 使用できる形式が複数ある場合、リスト内の形式の順序は、どの形式が使用されるかに影響しません。つまり、この順序で形式の優先順位は決まりません。 形式の選択の条件は、ネットワーク帯域幅などの外部要因に基づきます。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)
RawOutgoingVideoStreamOptions
を作成し、前に作成したオブジェクトで形式を設定します。var rawOutgoingVideoStreamOptions = RawOutgoingVideoStreamOptions() rawOutgoingVideoStreamOptions.formats = videoStreamFormats
前に作成した
RawOutgoingVideoStreamOptions
インスタンスを使用して、VirtualOutgoingVideoStream
のインスタンスを作成します。var rawOutgoingVideoStream = VirtualOutgoingVideoStream(videoStreamOptions: rawOutgoingVideoStreamOptions)
VirtualOutgoingVideoStreamDelegate
デリゲートに実装します。didChangeFormat
イベントは、リストで提供されているビデオ形式のいずれかからVideoStreamFormat
が変更されるたびに通知します。virtualOutgoingVideoStream.delegate = /* Attach delegate and implement didChangeFormat */
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 } }
VideoStreamPixelFormat.rgba
を使用してランダムなRawVideoFrameBuffer
を生成するために、次のヘルパー クラスのインスタンスを作成します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 } } }
VirtualOutgoingVideoStreamDelegate
に実装します。didChangeState
イベントは、現在のストリームの状態を通知します。 状態が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 } }
ScreenShare ビデオ
Windows システムではフレームが生成されるため、独自のフォアグラウンド サービスを実装してフレームをキャプチャし、Azure Communication Services Calling API を使用して送信する必要があります。
サポートされているビデオ解像度
[縦横比] | 解像度 | 最大 FPS |
---|---|---|
すべて | 任意 (最大 1080p) | 30 |
画面共有ビデオ ストリームを作成する手順
SDK でサポートされている VideoStreamPixelFormat を使用して
VideoFormat
の配列を作成します。 使用できる形式が複数ある場合、リスト内の形式の順序は、どの形式が使用されるかに影響しません。つまり、この順序で形式の優先順位は決まりません。 形式の選択の条件は、ネットワーク帯域幅などの外部要因に基づきます。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)
RawOutgoingVideoStreamOptions
を作成し、前に作成したオブジェクトでVideoFormats
を設定します。var rawOutgoingVideoStreamOptions = RawOutgoingVideoStreamOptions() rawOutgoingVideoStreamOptions.formats = videoStreamFormats
前に作成した
RawOutgoingVideoStreamOptions
インスタンスを使用して、VirtualOutgoingVideoStream
のインスタンスを作成します。var rawOutgoingVideoStream = ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions)
次の方法でビデオ フレームをキャプチャして送信します。
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*/ } }
未加工の着信ビデオ
この機能を使用すると、IncomingVideoStream
内のビデオ フレームにアクセスして、それらのストリーム オブジェクトをローカルで操作できます
VideoStreamKind.RawIncoming
を設定したJoinCallOptions
によって設定されるIncomingVideoOptions
のインスタンスを作成します。var incomingVideoOptions = IncomingVideoOptions() incomingVideoOptions.streamType = VideoStreamKind.rawIncoming var joinCallOptions = JoinCallOptions() joinCallOptions.incomingVideoOptions = incomingVideoOptions
ParticipantsUpdatedEventArgs
イベントを受信したら、RemoteParticipant.delegate.didChangedVideoStreamState
デリゲートをアタッチします。 このイベントは、IncomingVideoStream
オブジェクトの状態を通知します。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 } }
この時点では、
IncomingVideoStream
の状態はVideoStreamState.available
です。前の手順で示したように、RawIncomingVideoStream.delegate.didReceivedRawVideoFrame
デリゲートをアタッチします。 このイベントにより、新しいRawVideoFrame
オブジェクトが提供されます。func rawIncomingVideoStream(_ rawIncomingVideoStream: RawIncomingVideoStream, didRawVideoFrameReceived args: RawVideoFrameReceivedEventArgs) { /* Render/Modify/Save the video frame */ let videoFrame = args.frame as! RawVideoFrameBuffer }
開発者は、通話中に受信および送信のオーディオ、ビデオ、画面共有のコンテンツの未加工メディアにアクセスして、オーディオ/ビデオ コンテンツのキャプチャ、分析、処理を行うことができます。 Azure Communication Services クライアント側の未加工のオーディオ、未加工のビデオ、未加工の画面共有にアクセスすることにより、開発者は、Azure Communication Services Calling SDK 内で発生するオーディオ、ビデオ、画面共有のコンテンツをほぼ無制限に表示および編集できます。 このクイックスタートでは、JavaScript 用 Azure Communication Services Calling SDK を使用して、未加工メディア アクセスを実装する方法について説明します。
たとえば、次のように入力します。
- 通話オブジェクトで通話のオーディオ/ビデオ ストリームに直接アクセスしたり、通話中にカスタム発信オーディオ/ビデオ ストリームを送信したりできます。
- 分析用カスタム AI モデルを実行するためにオーディオおよびビデオ ストリームを検査できます。 このようなモデルには、エージェントの生産性向上を目的に、会話を分析したり、リアルタイムの分析情報や提案を提供したりするための自然言語処理が含まれる場合があります。
- 組織はオーディオおよびビデオ メディア ストリームを使用して、患者にバーチャル ケアを提供するときのセンチメントを分析したり、Mixed Reality を使用したビデオ通話中にリモート アシスタンスを提供したりできます。 この機能により、相互作用エクスペリエンスを強化するためのイノベーションを応用するための道が開発者に開かれます。
前提条件
重要
ここに挙げた例は、Calling SDK for JavaScript の 1.13.1 で利用可能です。 このクイックスタートを試す際は、必ずこのバージョン以降を使用してください。
未加工オーディオへのアクセス
未加工オーディオ メディアにアクセスすると、着信オーディオ ストリームにアクセスできると共に、通話中にカスタム発信オーディオ ストリームを表示および送信できます。
未加工の着信オーディオ ストリームにアクセスする
着信オーディオ ストリームにアクセスするには、次のコードを使用します。
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);
カスタム オーディオ ストリームを使用して通話を行う
ユーザーのマイク デバイスを使用する代わりにカスタム オーディオ ストリームを使用して通話を開始するには、次のコードを使用します。
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);
通話中にカスタム オーディオ ストリームに切り替える
通話中にユーザーのマイク デバイスを使用する代わりに、入力デバイスをカスタム オーディオ ストリームに切り替えるには、次のコードを使用します。
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);
カスタム オーディオ ストリームを停止する
カスタム オーディオ ストリームが通話中に設定された後に、その送信を停止するには、次のコードを使用します。
call.stopAudio();
未加工のビデオにアクセスする
未加工のビデオ メディアによって、MediaStream
オブジェクトのインスタンスが提供されます (詳細については、JavaScript のドキュメントを参照してください)。未加工のビデオ メディアは、特に着信および発信通話用の MediaStream
オブジェクトに対するアクセスを提供します。 未加工のビデオの場合、機械学習を使用してビデオのフレームを処理することにより、そのオブジェクトを使用してフィルターを適用できます。
処理された未加工の発信ビデオ フレームは、送信元の発信ビデオとして送信できます。 処理された未加工の着信ビデオ フレームは、受信側でレンダリングできます。
カスタム ビデオ ストリームを使用して通話を行う
未加工の発信ビデオ ストリームにアクセスできます。 未加工の発信ビデオ ストリームに MediaStream
を使用して、機械学習を使用してフレームを処理し、フィルターを適用します。 その後、処理された発信ビデオを送信元ビデオ ストリームとして送信できます。
この例では、キャンバス データを発信ビデオとしてユーザーに送信しています。
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);
通話中にカスタム ビデオ ストリームに切り替える
通話中にユーザーのカメラ デバイスを使用する代わりに、入力デバイスをカスタム ビデオ ストリームに切り替えるには、次のコードを使用します。
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);
カスタム ビデオ ストリームを停止する
カスタム ビデオ ストリームが通話中に設定された後に、その送信を停止するには、次のコードを使用します。
// Stop video by passing the same `localVideoStream` instance that was used to start video
await call.stopVideo(localVideoStream);
カスタム効果が適用されているカメラから別のカメラ デバイスに切り替える場合は、まずビデオを停止し、LocalVideoStream
のソースを切り替え、もう一度ビデオを開始します。
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);
}
リモート参加者から受信ビデオ ストリームにアクセスする
未加工の着信ビデオ ストリームにアクセスできます。 未加工の着信ビデオ ストリームに MediaStream
を使用して、機械学習を使用してフレームを処理し、フィルターを適用します。 その後、処理された着信ビデオを受信側でレンダリングできます。
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();
重要
Azure Communication Services のこの機能は、現在プレビュー段階にあります。
プレビューの API と SDK は、サービス レベル アグリーメントなしに提供されます。 運用環境のワークロードには使用しないことをお勧めします。 一部の機能はサポート対象ではなく、機能が制限されることがあります。
詳細については、「Microsoft Azure プレビューの追加利用規約」を確認してください。
未加工の画面共有アクセスはパブリック プレビュー段階であり、バージョン 1.15.1-beta.1 以降の一部として利用できます。
未加工の画面共有にアクセスする
未加工の画面共有メディアは、着信および発信の画面共有ストリームの MediaStream
オブジェクトに対するアクセスを具体的に提供します。 未加工の画面共有の場合、機械学習を使用して画面共有のフレームを処理することにより、そのオブジェクトを使用してフィルターを適用できます。
処理された未加工の画面共有フレームは、送信元の発信画面共有として送信できます。 処理された未加工の着信画面共有フレームは、受信側でレンダリングできます。
注: 画面共有の送信は、デスクトップ ブラウザーでのみサポートされています。
カスタム画面共有ストリームを使用して画面共有を開始する
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'
画面、ブラウザー タブ、またはアプリから未加工の画面共有ストリームにアクセスし、ストリームに効果を適用する
画面、ブラウザー タブ、またはアプリから未加工の画面共有ストリームに白黒効果を適用する方法の例を次に示します。 注: Canvas コンテキスト フィルター = "grayscale(1)" API は 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;
画面共有ストリームの送信を停止する
カスタム画面共有ストリームが通話中に設定された後に、その送信を停止するには、次のコードを使用します。
// Stop sending raw screen sharing stream
await call.stopScreenSharing(localScreenSharingStream);
リモート参加者からの着信画面共有ストリームにアクセスする
リモート参加者からの未加工の画面共有ストリームにアクセスできます。 未加工の着信画面共有ストリームに MediaStream
を使用して、機械学習を使用してフレームを処理し、フィルターを適用します。 その後、処理された着信画面共有ストリームを受信側でレンダリングできます。
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();
次のステップ
詳細については、次の記事を参照してください。
- 通話のヒーロー サンプルを確認する。
- UI ライブラリを使ってみる。
- Calling SDK の機能について確認する。
- 通話のしくみの詳細について確認する。