빠른 시작: 앱에 원시 미디어 액세스 추가

이 빠른 시작에서는 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:1 영상 통화를 앱에 추가의 단계를 수행하여 Unity 게임을 만듭니다. 목표는 호출을 시작할 준비가 된 CallAgent 개체를 가져오는 것입니다. GitHub에서 이 빠른 시작에 대한 최종 코드를 찾습니다.

  2. 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 };
    
  3. RawOutgoingVideoStreamOptions를 만들고, 이전에 만든 개체를 사용하여 Formats를 설정합니다.

    var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions
    {
        Formats = videoStreamFormats
    };
    
  4. 이전에 만든 RawOutgoingVideoStreamOptions 인스턴스를 사용하여 VirtualOutgoingVideoStream의 인스턴스를 만듭니다.

    var rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  5. RawOutgoingVideoStream.FormatChanged 대리자를 구독합니다. 이 이벤트는 VideoStreamFormat이 목록에 제공된 비디오 형식 중 하나에서 변경될 때마다 알려줍니다.

    rawOutgoingVideoStream.FormatChanged += (object sender, VideoStreamFormatChangedEventArgs args)
    {
        VideoStreamFormat videoStreamFormat = args.Format;
    }
    
  6. 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;
        }
    }
    
  7. 시작 및 중지와 같은 원시 발신 비디오 스트림 상태 트랜잭션을 처리하고 사용자 지정 비디오 프레임을 생성하거나 프레임 생성 알고리즘을 일시 중단합니다.

    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 옵션이 사용되도록 설정해야 합니다.

  8. 마찬가지로, 비디오 스트림 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 });
    }
    
  9. 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 액세스

원시 오디오 미디어에 액세스하면 수신 통화의 오디오 스트림에 액세스할 수 있을 뿐 아니라 통화 중에 사용자 지정 발신 오디오 스트림을 보고 보낼 수 있습니다.

원시 발신 오디오 보내기

보낼 원시 스트림 속성을 지정하는 옵션 개체를 만듭니다.

    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
  1. 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 };
    
  2. RawOutgoingVideoStreamOptions를 만들고, 이전에 만든 개체를 사용하여 Formats를 설정합니다.

    var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions
    {
        Formats = videoStreamFormats
    };
    
  3. 이전에 만든 RawOutgoingVideoStreamOptions 인스턴스를 사용하여 VirtualOutgoingVideoStream의 인스턴스를 만듭니다.

    var rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  4. RawOutgoingVideoStream.FormatChanged 대리자를 구독합니다. 이 이벤트는 VideoStreamFormat이 목록에 제공된 비디오 형식 중 하나에서 변경될 때마다 알려줍니다.

    rawOutgoingVideoStream.FormatChanged += (object sender, VideoStreamFormatChangedEventArgs args)
    {
        VideoStreamFormat videoStreamFormat = args.Format;
    }
    
  5. 다음 도우미 클래스 인스턴스를 만들어 버퍼 데이터에 액세스합니다.

    [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;
        }
    }
    
  6. 다음 도우미 클래스 인스턴스를 만들어 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;
            }
        }
    }
    
  7. 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

화면 공유 비디오 스트림을 만드는 단계

  1. 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 };
    
  2. RawOutgoingVideoStreamOptions를 만들고, 이전에 만든 개체를 사용하여 VideoFormats를 설정합니다.
    var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions
    {
        Formats = videoStreamFormats
    };
    
  3. 이전에 만든 RawOutgoingVideoStreamOptions 인스턴스를 사용하여 VirtualOutgoingVideoStream의 인스턴스를 만듭니다.
    var rawOutgoingVideoStream = new ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  4. 다음 방법으로 비디오 프레임을 캡처하고 보냅니다.
    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 내 비디오 프레임에 액세스할 수 있습니다.

  1. 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
    };
    
  2. 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;
        }
    }
    
  3. 이때 이전 단계와 같이 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
  1. 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);
    
  2. RawOutgoingVideoStreamOptions를 만들고, 이전에 만든 개체를 사용하여 Formats를 설정합니다.

    RawOutgoingVideoStreamOptions rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions();
    rawOutgoingVideoStreamOptions.setFormats(videoStreamFormats);
    
  3. 이전에 만든 RawOutgoingVideoStreamOptions 인스턴스를 사용하여 VirtualOutgoingVideoStream의 인스턴스를 만듭니다.

    VirtualOutgoingVideoStream rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  4. RawOutgoingVideoStream.addOnFormatChangedListener 대리자를 구독합니다. 이 이벤트는 VideoStreamFormat이 목록에 제공된 비디오 형식 중 하나에서 변경될 때마다 알려줍니다.

    virtualOutgoingVideoStream.addOnFormatChangedListener((VideoStreamFormatChangedEvent args) -> 
    {
        VideoStreamFormat videoStreamFormat = args.Format;
    });
    
  5. 다음 도우미 클래스 인스턴스를 만들어 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();
            }
        }
    }
    
  6. 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

화면 공유 비디오 스트림을 만드는 단계

  1. 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);
    
  2. RawOutgoingVideoStreamOptions를 만들고, 이전에 만든 개체를 사용하여 VideoFormats를 설정합니다.

    RawOutgoingVideoStreamOptions rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions();
    rawOutgoingVideoStreamOptions.setFormats(videoStreamFormats);
    
  3. 이전에 만든 RawOutgoingVideoStreamOptions 인스턴스를 사용하여 VirtualOutgoingVideoStream의 인스턴스를 만듭니다.

    ScreenShareOutgoingVideoStream rawOutgoingVideoStream = new ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  4. 다음 방법으로 비디오 프레임을 캡처하고 보냅니다.

    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 객체 내 비디오 프레임에 액세스할 수 있습니다.

  1. JoinCallOptions 설정 VideoStreamKind.RawIncoming을 통해 설정하는 IncomingVideoOptions 인스턴스를 만듭니다.

    IncomingVideoOptions incomingVideoOptions = new IncomingVideoOptions()
            .setStreamType(VideoStreamKind.RAW_INCOMING);
    
    JoinCallOptions joinCallOptions = new JoinCallOptions()
            .setIncomingVideoOptions(incomingVideoOptions);
    
  2. 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;
        }
    }
    
  3. 이때 이전 단계와 같이 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
  1. 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)
    
  2. RawOutgoingVideoStreamOptions를 만들고 이전에 만든 개체를 사용하여 형식을 설정합니다.

    var rawOutgoingVideoStreamOptions = RawOutgoingVideoStreamOptions()
    rawOutgoingVideoStreamOptions.formats = videoStreamFormats
    
  3. 이전에 만든 RawOutgoingVideoStreamOptions 인스턴스를 사용하여 VirtualOutgoingVideoStream의 인스턴스를 만듭니다.

    var rawOutgoingVideoStream = VirtualOutgoingVideoStream(videoStreamOptions: rawOutgoingVideoStreamOptions)
    
  4. VirtualOutgoingVideoStreamDelegate 대리자로 구현합니다. didChangeFormat 이벤트는 VideoStreamFormat이 이 목록에 제공된 비디오 형식 중 하나에서 변경될 때마다 알려줍니다.

    virtualOutgoingVideoStream.delegate = /* Attach delegate and implement didChangeFormat */
    
  5. 다음 도우미 클래스 인스턴스를 만들어 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
        }
    }
    
  6. 다음 도우미 클래스 인스턴스를 만들어 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
            }
        }
    }
    
  7. 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

화면 공유 비디오 스트림을 만드는 단계

  1. 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)
    
  2. RawOutgoingVideoStreamOptions를 만들고, 이전에 만든 개체를 사용하여 VideoFormats를 설정합니다.

    var rawOutgoingVideoStreamOptions = RawOutgoingVideoStreamOptions()
    rawOutgoingVideoStreamOptions.formats = videoStreamFormats
    
  3. 이전에 만든 RawOutgoingVideoStreamOptions 인스턴스를 사용하여 VirtualOutgoingVideoStream의 인스턴스를 만듭니다.

    var rawOutgoingVideoStream = ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions)
    
  4. 다음 방법으로 비디오 프레임을 캡처하고 보냅니다.

    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 내 비디오 프레임에 액세스할 수 있습니다.

  1. JoinCallOptions 설정 VideoStreamKind.RawIncoming을 통해 설정하는 IncomingVideoOptions 인스턴스를 만듭니다.

    var incomingVideoOptions = IncomingVideoOptions()
    incomingVideoOptions.streamType = VideoStreamKind.rawIncoming
    var joinCallOptions = JoinCallOptions()
    joinCallOptions.incomingVideoOptions = incomingVideoOptions
    
  2. 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
        }
    }
    
  3. 이때 이전 단계와 같이 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();

다음 단계

자세한 내용은 다음 문서를 참조하세요.