빠른 시작: 앱에 원시 미디어 액세스 추가
이 빠른 시작에서는 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 |
---|---|---|
16x9 | 1080p | 30 |
16x9 | 720p | 30 |
16x9 | 540p | 30 |
16x9 | 480p | 30 |
16x9 | 360p | 30 |
16x9 | 270p | 15 |
16x9 | 240p | 15 |
16x9 | 180p | 15 |
4x3 | VGA(640x480) | 30 |
4x3 | 424x320 | 15 |
4x3 | QVGA(320x240) | 15 |
4x3 | 212x160 | 15 |
빠른 시작: 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 }; }
참고 항목
NativeBuffer
를 사용하려면 네이티브 메모리 리소스에 액세스해야 하므로unsafe
한정자가 이 메서드에 사용됩니다. 따라서 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
를 호출하여 수신 프레임을 UnityVideoTexture
에 렌더링하는 방법에 대한 샘플은 다음과 같습니다.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 액세스
원시 오디오 미디어에 액세스하면 수신 통화의 오디오 스트림에 액세스할 수 있을 뿐 아니라 통화 중에 사용자 지정 발신 오디오 스트림을 보고 보낼 수 있습니다.
원시 발신 오디오 보내기
보낼 원시 스트림 속성을 지정하는 옵션 개체를 만듭니다.
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 |
---|---|---|
16x9 | 1080p | 30 |
16x9 | 720p | 30 |
16x9 | 540p | 30 |
16x9 | 480p | 30 |
16x9 | 360p | 30 |
16x9 | 270p | 15 |
16x9 | 240p | 15 |
16x9 | 180p | 15 |
4x3 | VGA(640x480) | 30 |
4x3 | 424x320 | 15 |
4x3 | QVGA(320x240) | 15 |
4x3 | 212x160 | 15 |
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
내 비디오 프레임에 액세스할 수 있습니다.
JoinCallOptions
설정VideoStreamKind.RawIncoming
을 통해 설정하는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 액세스
원시 오디오 미디어에 액세스하면 통화의 수신 오디오 스트림에 액세스할 수 있을 뿐 아니라 통화 중에 사용자 지정 발신 오디오 스트림을 보고 보낼 수 있습니다.
원시 발신 오디오 보내기
보낼 원시 스트림 속성을 지정하는 옵션 개체를 만듭니다.
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 |
---|---|---|
16x9 | 1080p | 30 |
16x9 | 720p | 30 |
16x9 | 540p | 30 |
16x9 | 480p | 30 |
16x9 | 360p | 30 |
16x9 | 270p | 15 |
16x9 | 240p | 15 |
16x9 | 180p | 15 |
4x3 | VGA(640x480) | 30 |
4x3 | 424x320 | 15 |
4x3 | QVGA(320x240) | 15 |
4x3 | 212x160 | 15 |
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
객체 내 비디오 프레임에 액세스할 수 있습니다.
JoinCallOptions
설정VideoStreamKind.RawIncoming
을 통해 설정하는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 액세스
원시 오디오 미디어에 액세스하면 수신 통화의 오디오 스트림에 액세스할 수 있을 뿐 아니라 통화 중에 사용자 지정 발신 오디오 스트림을 보고 보낼 수 있습니다.
원시 발신 오디오 보내기
보낼 원시 스트림 속성을 지정하는 옵션 개체를 만듭니다.
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
인 경우에만 데이터를 전송할 수 있습니다.
오디오 스트림 상태 변경을 관찰하기 위해 Microsoft에서 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()
}
}
참고 항목
오디오 엔진 입력 노드의 샘플 속도 기본값은 공유 오디오 세션의 기본 샘플 속도 > 값으로 설정됩니다. 따라서 다른 값을 사용하여 해당 노드에 탭을 설치할 수 없습니다.
따라서 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 |
---|---|---|
16x9 | 1080p | 30 |
16x9 | 720p | 30 |
16x9 | 540p | 30 |
16x9 | 480p | 30 |
16x9 | 360p | 30 |
16x9 | 270p | 15 |
16x9 | 240p | 15 |
16x9 | 180p | 15 |
4x3 | VGA(640x480) | 30 |
4x3 | 424x320 | 15 |
4x3 | QVGA(320x240) | 15 |
4x3 | 212x160 | 15 |
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
내 비디오 프레임에 액세스할 수 있습니다.
JoinCallOptions
설정VideoStreamKind.RawIncoming
을 통해 설정하는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 모델을 실행하고 분석할 수 있습니다. 이러한 모델에는 대화를 분석하거나 에이전트 생산성을 높이기 위한 실시간 인사이트 및 제안을 제공하는 자연어 처리가 포함될 수 있습니다.
- 조직에서는 오디오 및 비디오 미디어 스트림을 사용하여 환자에게 가상 치료를 제공할 때 감정을 분석하거나 혼합 현실을 사용하는 화상 통화 중에 원격 지원을 제공할 수 있습니다. 이 기능은 개발자가 혁신을 통해 상호 작용 경험을 향상할 수 있는 길을 열어줍니다.
필수 조건
Important
이 예제는 JavaScript용 Calling SDK 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();
Important
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 기능에 대해 알아보세요.
- 통화 작동 방식에 대해 알아봅니다.