Delen via


Quickstart: Onbewerkte mediatoegang toevoegen aan uw app

In deze quickstart leert u hoe u onbewerkte mediatoegang implementeert met behulp van de Azure Communication Services Calling SDK voor Unity. De Azure Communication Services Calling SDK biedt API's waarmee apps hun eigen videoframes kunnen genereren om onbewerkte videoframes te verzenden of weer te geven van externe deelnemers aan een gesprek. Deze quickstart bouwt voort op quickstart: 1:1 videogesprekken toevoegen aan uw app voor Unity.

RawVideo-toegang

Omdat de app de videoframes genereert, moet de app de Azure Communication Services Calling SDK informeren over de video-indelingen die de app kan genereren. Met deze informatie kan de Sdk voor aanroepen van Azure Communication Services de beste configuratie van de video-indeling kiezen voor de netwerkvoorwaarden op dat moment.

Virtuele video

Ondersteunde videoresoluties

Hoogte-breedteverhouding Oplossing Maximum 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. Volg de stappen in deze quickstart: 1:1 videogesprekken toevoegen aan uw app om unity-games te maken. Het doel is om een CallAgent object te verkrijgen dat gereed is om de aanroep te starten. Zoek de voltooide code voor deze quickstart op GitHub.

  2. Maak een matrix van het gebruik van VideoFormat de VideoStream PixelFormat die door de SDK wordt ondersteund. Wanneer er meerdere indelingen beschikbaar zijn, heeft de volgorde van de indelingen in de lijst geen invloed op of geeft deze prioriteit aan welke wordt gebruikt. De criteria voor het selecteren van opmaak zijn gebaseerd op externe factoren, zoals netwerkbandbreedte.

    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. Maak RawOutgoingVideoStreamOptionsen stel deze in Formats met het eerder gemaakte object.

    var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions
    {
        Formats = videoStreamFormats
    };
    
  4. Maak een exemplaar van met behulp van VirtualOutgoingVideoStream het RawOutgoingVideoStreamOptions exemplaar dat u eerder hebt gemaakt.

    var rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  5. Abonneer u op de RawOutgoingVideoStream.FormatChanged gemachtigde. Met deze gebeurtenis wordt opgegeven wanneer de VideoStreamFormat video is gewijzigd van een van de video-indelingen in de lijst.

    rawOutgoingVideoStream.FormatChanged += (object sender, VideoStreamFormatChangedEventArgs args)
    {
        VideoStreamFormat videoStreamFormat = args.Format;
    }
    
  6. Abonneer u op de RawOutgoingVideoStream.StateChanged gemachtigde. Met deze gebeurtenis wordt opgegeven wanneer de State wijziging is doorgevoerd.

    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. Verwerking van onbewerkte uitgaande transacties voor videostreams, zoals Starten en Stoppen en beginnen met het genereren van aangepaste videoframes of het algoritme voor het genereren van frame onderbreken.

    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;
        }
    }
    

    Hier volgt een voorbeeld van een uitgaande videoframegenerator:

    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
        };
    }
    

    Notitie

    unsafe modifier wordt gebruikt voor deze methode omdat NativeBuffer toegang tot systeemeigen geheugenbronnen is vereist. Allow unsafe Daarom moet de optie ook worden ingeschakeld in Unity Editor.

  8. Op dezelfde manier kunnen we binnenkomende videoframes verwerken als reactie op de gebeurtenis van de videostream 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. Het wordt ten zeerste aanbevolen om zowel binnenkomende als uitgaande videoframes te beheren via een buffermechanisme om overbelasting van de MonoBehaviour.Update() callback-methode te voorkomen, die licht moet worden gehouden en cpu- of netwerkzware taken moet voorkomen en een soepelere video-ervaring te garanderen. Deze optionele optimalisatie wordt aan ontwikkelaars overgelaten om te bepalen wat het beste werkt in hun scenario's.

    Hier volgt een voorbeeld van hoe de binnenkomende frames kunnen worden weergegeven in een Unity VideoTexture door een interne wachtrij aan te roepen Graphics.Blit :

    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();
        }
    }
    

In deze quickstart leert u hoe u onbewerkte mediatoegang implementeert met behulp van de Azure Communication Services Calling SDK voor Windows. De Azure Communication Services Calling SDK biedt API's waarmee apps hun eigen videoframes kunnen genereren om te verzenden naar externe deelnemers in een gesprek. Deze quickstart bouwt voort op quickstart: 1:1 videogesprekken toevoegen aan uw app voor Windows.

RawAudio-toegang

Als u toegang krijgt tot onbewerkte audiomedia, hebt u toegang tot de audiostream van het binnenkomende gesprek, samen met de mogelijkheid om aangepaste uitgaande audiostreams tijdens een gesprek weer te geven en te verzenden.

Onbewerkte uitgaande audio verzenden

Maak een optiesobject waarin de onbewerkte stroomeigenschappen worden opgegeven die we willen verzenden.

    RawOutgoingAudioStreamProperties outgoingAudioProperties = new RawOutgoingAudioStreamProperties()
    {
        Format = ACSAudioStreamFormat.Pcm16Bit,
        SampleRate = AudioStreamSampleRate.Hz48000,
        ChannelMode = AudioStreamChannelMode.Stereo,
        BufferDuration = AudioStreamBufferDuration.InMs20
    };
    RawOutgoingAudioStreamOptions outgoingAudioStreamOptions = new RawOutgoingAudioStreamOptions()
    {
        Properties = outgoingAudioProperties
    };

Maak een RawOutgoingAudioStream en voeg deze toe om deel te nemen aan gespreksopties en de stream wordt automatisch gestart wanneer de oproep is verbonden.

    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.

Stream koppelen aan een gesprek

U kunt de stream ook koppelen aan een bestaand Call exemplaar:

    await call.StartAudio(rawOutgoingAudioStream);

Beginnen met het verzenden van onbewerkte voorbeelden

We kunnen alleen beginnen met het verzenden van gegevens zodra de stroomstatus is AudioStreamState.Started. Als u de status van de audiostream wilt bekijken, voegt u een listener toe aan de OnStateChangedListener gebeurtenis.

    unsafe private void AudioStateChanged(object sender, AudioStreamStateChanged args)
    {
        if (args.AudioStreamState == AudioStreamState.Started)
        {
            // We can now start sending samples.
        }
    }
    outgoingAudioStream.StateChanged += AudioStateChanged;

Wanneer de stream is gestart, kunnen we beginnen met het verzenden van MemoryBuffer audiovoorbeelden naar het gesprek. De audiobufferindeling moet overeenkomen met de opgegeven stroomeigenschappen.

    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();
    }

Raw-audio ontvangen

We kunnen ook de voorbeelden van de gespreksaudiostream ontvangen alsof MemoryBuffer we de audiostream van de oproep willen verwerken voordat ze worden afgespeeld. Maak een RawIncomingAudioStreamOptions object waarin de onbewerkte stroomeigenschappen worden opgegeven die we willen ontvangen.

    RawIncomingAudioStreamProperties properties = new RawIncomingAudioStreamProperties()
    {
        Format = AudioStreamFormat.Pcm16Bit,
        SampleRate = AudioStreamSampleRate.Hz44100,
        ChannelMode = AudioStreamChannelMode.Stereo
    };
    RawIncomingAudioStreamOptions options = new RawIncomingAudioStreamOptions()
    {
        Properties = properties
    };

Maak een RawIncomingAudioStream en voeg deze toe om deel te nemen aan gespreksopties

    JoinCallOptions options =  JoinCallOptions(); // or StartCallOptions()
    RawIncomingAudioStream rawIncomingAudioStream = new RawIncomingAudioStream(audioStreamOptions);
    IncomingAudioOptions incomingAudioOptions = new IncomingAudioOptions()
    {
        Stream = rawIncomingAudioStream
    };
    options.IncomingAudioOptions = incomingAudioOptions;

Of we kunnen de stream ook koppelen aan een bestaand Call exemplaar:

    await call.startAudio(context, rawIncomingAudioStream);

Als u onbewerkte audiobuffers van de binnenkomende stream wilt ontvangen, voegt u listeners toe aan de status van de binnenkomende stroom en buffer ontvangen gebeurtenissen.

    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-toegang

Omdat de app de videoframes genereert, moet de app de Azure Communication Services Calling SDK informeren over de video-indelingen die de app kan genereren. Met deze informatie kan de Sdk voor aanroepen van Azure Communication Services de beste configuratie van de video-indeling kiezen voor de netwerkvoorwaarden op dat moment.

Virtuele video

Ondersteunde videoresoluties

Hoogte-breedteverhouding Oplossing Maximum 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. Maak een matrix van het gebruik van VideoFormat de VideoStream PixelFormat die door de SDK wordt ondersteund. Wanneer er meerdere indelingen beschikbaar zijn, heeft de volgorde van de indelingen in de lijst geen invloed op of geeft deze prioriteit aan welke wordt gebruikt. De criteria voor het selecteren van opmaak zijn gebaseerd op externe factoren, zoals netwerkbandbreedte.

    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. Maak RawOutgoingVideoStreamOptionsen stel deze in Formats met het eerder gemaakte object.

    var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions
    {
        Formats = videoStreamFormats
    };
    
  3. Maak een exemplaar van met behulp van VirtualOutgoingVideoStream het RawOutgoingVideoStreamOptions exemplaar dat u eerder hebt gemaakt.

    var rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  4. Abonneer u op de RawOutgoingVideoStream.FormatChanged gemachtigde. Met deze gebeurtenis wordt opgegeven wanneer de VideoStreamFormat video is gewijzigd van een van de video-indelingen in de lijst.

    rawOutgoingVideoStream.FormatChanged += (object sender, VideoStreamFormatChangedEventArgs args)
    {
        VideoStreamFormat videoStreamFormat = args.Format;
    }
    
  5. Een exemplaar van de volgende helperklasse maken voor toegang tot de buffergegevens

    [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. Een exemplaar van de volgende helperklasse maken om willekeurige RawVideoFrameexemplaren te genereren met behulp van VideoStreamPixelFormat.Rgba

    public class VideoFrameSender
    {
        private RawOutgoingVideoStream rawOutgoingVideoStream;
        private RawVideoFrameKind rawVideoFrameKind;
        private Thread frameIteratorThread;
        private Random random = new Random();
        private volatile bool stopFrameIterator = false;
        public VideoFrameSender(RawVideoFrameKind rawVideoFrameKind, RawOutgoingVideoStream rawOutgoingVideoStream)
        {
            this.rawVideoFrameKind = rawVideoFrameKind;
            this.rawOutgoingVideoStream = rawOutgoingVideoStream;
        }
        public async void VideoFrameIterator()
        {
            while (!stopFrameIterator)
            {
                if (rawOutgoingVideoStream != null &&
                    rawOutgoingVideoStream.Format != null &&
                    rawOutgoingVideoStream.State == VideoStreamState.Started)
                {
                    await SendRandomVideoFrameRGBA();
                }
            }
        }
        private async Task SendRandomVideoFrameRGBA()
        {
            uint rgbaCapacity = (uint)(rawOutgoingVideoStream.Format.Width * rawOutgoingVideoStream.Format.Height * 4);
            RawVideoFrame videoFrame = null;
            switch (rawVideoFrameKind)
            {
                case RawVideoFrameKind.Buffer:
                    videoFrame = 
                        GenerateRandomVideoFrameBuffer(rawOutgoingVideoStream.Format, rgbaCapacity);
                    break;
                case RawVideoFrameKind.Texture:
                    videoFrame = 
                        GenerateRandomVideoFrameTexture(rawOutgoingVideoStream.Format, rgbaCapacity);
                    break;
            }
            try
            {
                using (videoFrame)
                {
                    await rawOutgoingVideoStream.SendRawVideoFrameAsync(videoFrame);
                }
            }
            catch (Exception ex)
            {
                string msg = ex.Message;
            }
            try
            {
                int delayBetweenFrames = (int)(1000.0 / rawOutgoingVideoStream.Format.FramesPerSecond);
                await Task.Delay(delayBetweenFrames);
            }
            catch (Exception ex)
            {
                string msg = ex.Message;
            }
        }
        private unsafe RawVideoFrame GenerateRandomVideoFrameBuffer(VideoStreamFormat videoFormat, uint rgbaCapacity)
        {
            var rgbaBuffer = new MemoryBuffer(rgbaCapacity);
            byte* rgbaArrayBuffer = BufferExtensions.GetArrayBuffer(rgbaBuffer);
            GenerateRandomVideoFrame(&rgbaArrayBuffer);
            return new RawVideoFrameBuffer()
            {
                Buffers = new MemoryBuffer[] { rgbaBuffer },
                StreamFormat = videoFormat
            };
        }
        private unsafe RawVideoFrame GenerateRandomVideoFrameTexture(VideoStreamFormat videoFormat, uint rgbaCapacity)
        {
            var timeSpan = new TimeSpan(rawOutgoingVideoStream.TimestampInTicks);
            var rgbaBuffer = new Buffer(rgbaCapacity)
            {
                Length = rgbaCapacity
            };
            byte* rgbaArrayBuffer = BufferExtensions.GetArrayBuffer(rgbaBuffer);
            GenerateRandomVideoFrame(&rgbaArrayBuffer);
            var mediaStreamSample = MediaStreamSample.CreateFromBuffer(rgbaBuffer, timeSpan);
            return new RawVideoFrameTexture()
            {
                Texture = mediaStreamSample,
                StreamFormat = videoFormat
            };
        }
        private unsafe void GenerateRandomVideoFrame(byte** rgbaArrayBuffer)
        {
            int w = rawOutgoingVideoStream.Format.Width;
            int h = rawOutgoingVideoStream.Format.Height;
            byte r = (byte)random.Next(1, 255);
            byte g = (byte)random.Next(1, 255);
            byte b = (byte)random.Next(1, 255);
            int rgbaStride = w * 4;
            for (int y = 0; y < h; y++)
            {
                for (int x = 0; x < rgbaStride; x += 4)
                {
                    (*rgbaArrayBuffer)[(w * 4 * y) + x + 0] = (byte)(y % r);
                    (*rgbaArrayBuffer)[(w * 4 * y) + x + 1] = (byte)(y % g);
                    (*rgbaArrayBuffer)[(w * 4 * y) + x + 2] = (byte)(y % b);
                    (*rgbaArrayBuffer)[(w * 4 * y) + x + 3] = 255;
                }
            }
        }
        public void Start()
        {
            frameIteratorThread = new Thread(VideoFrameIterator);
            frameIteratorThread.Start();
        }
        public void Stop()
        {
            try
            {
                if (frameIteratorThread != null)
                {
                    stopFrameIterator = true;
                    frameIteratorThread.Join();
                    frameIteratorThread = null;
                    stopFrameIterator = false;
                }
            }
            catch (Exception ex)
            {
                string msg = ex.Message;
            }
        }
    }
    
  7. Abonneer u op de VideoStream.StateChanged gemachtigde. Met deze gebeurtenis wordt de status van de huidige stream geïnformeerd. Verzend geen frames als de status niet gelijk is aan 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;
            }
    };
    

Video delen op het scherm

Omdat het Windows-systeem de frames genereert, moet u uw eigen voorgrondservice implementeren om de frames vast te leggen en te verzenden met behulp van de Api voor aanroepen van Azure Communication Services.

Ondersteunde videoresoluties

Hoogte-breedteverhouding Oplossing Maximum FPS
Iets Alles tot 1080p 30

Stappen voor het maken van een videostream voor het delen van een scherm

  1. Maak een matrix van het gebruik van VideoFormat de VideoStream PixelFormat die door de SDK wordt ondersteund. Wanneer er meerdere indelingen beschikbaar zijn, heeft de volgorde van de indelingen in de lijst geen invloed op of geeft deze prioriteit aan welke wordt gebruikt. De criteria voor het selecteren van opmaak zijn gebaseerd op externe factoren, zoals netwerkbandbreedte.
    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. Maak RawOutgoingVideoStreamOptionsen stel deze in VideoFormats met het eerder gemaakte object.
    var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions
    {
        Formats = videoStreamFormats
    };
    
  3. Maak een exemplaar van met behulp van VirtualOutgoingVideoStream het RawOutgoingVideoStreamOptions exemplaar dat u eerder hebt gemaakt.
    var rawOutgoingVideoStream = new ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  4. Leg het videoframe op de volgende manier vast en verzend het.
    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;
        }
    }
    

Onbewerkte binnenkomende video

Met deze functie krijgt u toegang tot de videoframes in de IncomingVideoStream's om deze streams lokaal te bewerken

  1. Een exemplaar van IncomingVideoOptions die sets maken via JoinCallOptions instelling VideoStreamKind.RawIncoming
    var frameKind = RawVideoFrameKind.Buffer;  // Use the frameKind you prefer to receive
    var incomingVideoOptions = new IncomingVideoOptions
    {
        StreamKind = VideoStreamKind.RawIncoming,
        FrameKind = frameKind
    };
    var joinCallOptions = new JoinCallOptions
    {
        IncomingVideoOptions = incomingVideoOptions
    };
    
  2. Zodra u een gedelegeerde voor het koppelen RemoteParticipant.VideoStreamStateChanged van gebeurtenissen ParticipantsUpdatedEventArgs ontvangt. Met deze gebeurtenis wordt de status van de IncomingVideoStream objecten geïnformeerd.
    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. Op het moment heeft VideoStreamState.Available de IncomingVideoStream status gedelegeerde voor het koppelen RawIncomingVideoStream.RawVideoFrameReceived van de status, zoals wordt weergegeven in de vorige stap. Dit biedt de nieuwe RawVideoFrame objecten.
    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;
        }
    }
    

In deze quickstart leert u hoe u onbewerkte mediatoegang implementeert met behulp van de Azure Communication Services Calling SDK voor Android.

De Azure Communication Services Calling SDK biedt API's waarmee apps hun eigen videoframes kunnen genereren om te verzenden naar externe deelnemers in een gesprek.

Deze quickstart bouwt voort op quickstart: 1:1 videogesprekken toevoegen aan uw app voor Android.

RawAudio-toegang

Door toegang te krijgen tot onbewerkte audiomedia hebt u toegang tot de binnenkomende audiostream van het gesprek, samen met de mogelijkheid om aangepaste uitgaande audiostreams tijdens een gesprek weer te geven en te verzenden.

Onbewerkte uitgaande audio verzenden

Maak een optiesobject waarin de onbewerkte stroomeigenschappen worden opgegeven die we willen verzenden.

    RawOutgoingAudioStreamProperties outgoingAudioProperties = new RawOutgoingAudioStreamProperties()
                .setAudioFormat(AudioStreamFormat.PCM16_BIT)
                .setSampleRate(AudioStreamSampleRate.HZ44100)
                .setChannelMode(AudioStreamChannelMode.STEREO)
                .setBufferDuration(AudioStreamBufferDuration.IN_MS20);

    RawOutgoingAudioStreamOptions outgoingAudioStreamOptions = new RawOutgoingAudioStreamOptions()
                .setProperties(outgoingAudioProperties);

Maak een RawOutgoingAudioStream en voeg deze toe om deel te nemen aan gespreksopties en de stream wordt automatisch gestart wanneer de oproep is verbonden.

    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.

Stream koppelen aan een gesprek

U kunt de stream ook koppelen aan een bestaand Call exemplaar:

    CompletableFuture<Void> result = call.startAudio(context, rawOutgoingAudioStream);

Beginnen met het verzenden van onbewerkte voorbeelden

We kunnen alleen beginnen met het verzenden van gegevens zodra de stroomstatus is AudioStreamState.STARTED. Als u de status van de audiostream wilt bekijken, voegt u een listener toe aan de OnStateChangedListener gebeurtenis.

    private void onStateChanged(PropertyChangedEvent propertyChangedEvent) {
        // When value is `AudioStreamState.STARTED` we'll be able to send audio samples.
    }

    rawOutgoingAudioStream.addOnStateChangedListener(this::onStateChanged)

Wanneer de stream is gestart, kunnen we beginnen met het verzenden van java.nio.ByteBuffer audiovoorbeelden naar het gesprek.

De audiobufferindeling moet overeenkomen met de opgegeven stroomeigenschappen.

    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();

Raw-audio ontvangen

We kunnen ook de voorbeelden van de gespreksaudiostream ontvangen alsof java.nio.ByteBuffer we de audio willen verwerken voordat ze worden afgespeeld.

Maak een RawIncomingAudioStreamOptions object waarin de onbewerkte stroomeigenschappen worden opgegeven die we willen ontvangen.

    RawIncomingAudioStreamOptions options = new RawIncomingAudioStreamOptions();
    RawIncomingAudioStreamProperties properties = new RawIncomingAudioStreamProperties()
                .setAudioFormat(AudioStreamFormat.PCM16_BIT)
                .setSampleRate(AudioStreamSampleRate.HZ44100)
                .setChannelMode(AudioStreamChannelMode.STEREO);
    options.setProperties(properties);

Maak een RawIncomingAudioStream en voeg deze toe om deel te nemen aan gespreksopties

    JoinCallOptions options =  JoinCallOptions() // or StartCallOptions()
    IncomingAudioOptions incomingAudioOptions = new IncomingAudioOptions();

    RawIncomingAudioStream rawIncomingAudioStream = new RawIncomingAudioStream(audioStreamOptions);
    incomingAudioOptions.setStream(rawIncomingAudioStream);
    options.setIncomingAudioOptions(incomingAudioOptions);

Of we kunnen de stream ook koppelen aan een bestaand Call exemplaar:


    CompletableFuture<Void> result = call.startAudio(context, rawIncomingAudioStream);

Als u onbewerkte audiobuffers van de binnenkomende stream wilt ontvangen, voegt u listeners toe aan de status van de binnenkomende stroom en buffer ontvangen gebeurtenissen.

    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);

Het is ook belangrijk om te onthouden dat u de audiostream in het huidige gespreksexemplaren Call stopt:


    CompletableFuture<Void> result = call.stopAudio(context, rawIncomingAudioStream);

RawVideo-toegang

Omdat de app de videoframes genereert, moet de app de Azure Communication Services Calling SDK informeren over de video-indelingen die de app kan genereren. Met deze informatie kan de Sdk voor aanroepen van Azure Communication Services de beste configuratie van de video-indeling kiezen voor de netwerkvoorwaarden op dat moment.

Virtuele video

Ondersteunde videoresoluties

Hoogte-breedteverhouding Oplossing Maximum 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. Maak een matrix van het gebruik van VideoFormat de VideoStream PixelFormat die door de SDK wordt ondersteund.

    Wanneer er meerdere indelingen beschikbaar zijn, heeft de volgorde van de indelingen in de lijst geen invloed op of geeft deze prioriteit aan welke wordt gebruikt. De criteria voor het selecteren van opmaak zijn gebaseerd op externe factoren, zoals netwerkbandbreedte.

    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. Maak RawOutgoingVideoStreamOptionsen stel deze in Formats met het eerder gemaakte object.

    RawOutgoingVideoStreamOptions rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions();
    rawOutgoingVideoStreamOptions.setFormats(videoStreamFormats);
    
  3. Maak een exemplaar van met behulp van VirtualOutgoingVideoStream het RawOutgoingVideoStreamOptions exemplaar dat u eerder hebt gemaakt.

    VirtualOutgoingVideoStream rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  4. Abonneer u op de RawOutgoingVideoStream.addOnFormatChangedListener gemachtigde. Met deze gebeurtenis wordt opgegeven wanneer de VideoStreamFormat video is gewijzigd van een van de video-indelingen in de lijst.

    virtualOutgoingVideoStream.addOnFormatChangedListener((VideoStreamFormatChangedEvent args) -> 
    {
        VideoStreamFormat videoStreamFormat = args.Format;
    });
    
  5. Een exemplaar van de volgende helperklasse maken om willekeurige RawVideoFrameexemplaren te genereren met behulp van VideoStreamPixelFormat.RGBA

    public class VideoFrameSender
    {
        private RawOutgoingVideoStream rawOutgoingVideoStream;
        private Thread frameIteratorThread;
        private Random random = new Random();
        private volatile boolean stopFrameIterator = false;
    
        public VideoFrameSender(RawOutgoingVideoStream rawOutgoingVideoStream)
        {
            this.rawOutgoingVideoStream = rawOutgoingVideoStream;
        }
    
        public void VideoFrameIterator()
        {
            while (!stopFrameIterator)
            {
                if (rawOutgoingVideoStream != null && 
                    rawOutgoingVideoStream.getFormat() != null && 
                    rawOutgoingVideoStream.getState() == VideoStreamState.STARTED)
                {
                    SendRandomVideoFrameRGBA();
                }
            }
        }
    
        private void SendRandomVideoFrameRGBA()
        {
            int rgbaCapacity = rawOutgoingVideoStream.getFormat().getWidth() * rawOutgoingVideoStream.getFormat().getHeight() * 4;
    
            RawVideoFrame videoFrame = GenerateRandomVideoFrameBuffer(rawOutgoingVideoStream.getFormat(), rgbaCapacity);
    
            try
            {
                rawOutgoingVideoStream.sendRawVideoFrame(videoFrame).get();
    
                int delayBetweenFrames = (int)(1000.0 / rawOutgoingVideoStream.getFormat().getFramesPerSecond());
                Thread.sleep(delayBetweenFrames);
            }
            catch (Exception ex)
            {
                String msg = ex.getMessage();
            }
            finally
            {
                videoFrame.close();
            }
        }
    
        private RawVideoFrame GenerateRandomVideoFrameBuffer(VideoStreamFormat videoStreamFormat, int rgbaCapacity)
        {
            ByteBuffer rgbaBuffer = ByteBuffer.allocateDirect(rgbaCapacity); // Only allocateDirect ByteBuffers are allowed
            rgbaBuffer.order(ByteOrder.nativeOrder());
    
            GenerateRandomVideoFrame(rgbaBuffer, rgbaCapacity);
    
            RawVideoFrameBuffer videoFrameBuffer = new RawVideoFrameBuffer();
            videoFrameBuffer.setBuffers(Arrays.asList(rgbaBuffer));
            videoFrameBuffer.setStreamFormat(videoStreamFormat);
    
            return videoFrameBuffer;
        }
    
        private void GenerateRandomVideoFrame(ByteBuffer rgbaBuffer, int rgbaCapacity)
        {
            int w = rawOutgoingVideoStream.getFormat().getWidth();
            int h = rawOutgoingVideoStream.getFormat().getHeight();
    
            byte rVal = (byte)random.nextInt(255);
            byte gVal = (byte)random.nextInt(255);
            byte bVal = (byte)random.nextInt(255);
            byte aVal = (byte)255;
    
            byte[] rgbaArrayBuffer = new byte[rgbaCapacity];
    
            int rgbaStride = w * 4;
    
            for (int y = 0; y < h; y++)
            {
                for (int x = 0; x < rgbaStride; x += 4)
                {
                    rgbaArrayBuffer[(w * 4 * y) + x + 0] = rVal;
                    rgbaArrayBuffer[(w * 4 * y) + x + 1] = gVal;
                    rgbaArrayBuffer[(w * 4 * y) + x + 2] = bVal;
                    rgbaArrayBuffer[(w * 4 * y) + x + 3] = aVal;
                }
            }
    
            rgbaBuffer.put(rgbaArrayBuffer);
            rgbaBuffer.rewind();
        }
    
        public void Start()
        {
            frameIteratorThread = new Thread(this::VideoFrameIterator);
            frameIteratorThread.start();
        }
    
        public void Stop()
        {
            try
            {
                if (frameIteratorThread != null)
                {
                    stopFrameIterator = true;
    
                    frameIteratorThread.join();
                    frameIteratorThread = null;
    
                    stopFrameIterator = false;
                }
            }
            catch (InterruptedException ex)
            {
                String msg = ex.getMessage();
            }
        }
    }
    
  6. Abonneer u op de VideoStream.addOnStateChangedListener gemachtigde. Deze gemachtigde informeert de status van de huidige stream. Verzend geen frames als de status niet gelijk is aan 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 Video

Omdat het Windows-systeem de frames genereert, moet u uw eigen voorgrondservice implementeren om de frames vast te leggen en te verzenden met behulp van de Api voor aanroepen van Azure Communication Services.

Ondersteunde videoresoluties

Hoogte-breedteverhouding Oplossing Maximum FPS
Iets Alles tot 1080p 30

Stappen voor het maken van een videostream voor het delen van een scherm

  1. Maak een matrix van het gebruik van VideoFormat de VideoStream PixelFormat die door de SDK wordt ondersteund.

    Wanneer er meerdere indelingen beschikbaar zijn, heeft de volgorde van de indelingen in de lijst geen invloed op of geeft deze prioriteit aan welke wordt gebruikt. De criteria voor het selecteren van opmaak zijn gebaseerd op externe factoren, zoals netwerkbandbreedte.

    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. Maak RawOutgoingVideoStreamOptionsen stel deze in VideoFormats met het eerder gemaakte object.

    RawOutgoingVideoStreamOptions rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions();
    rawOutgoingVideoStreamOptions.setFormats(videoStreamFormats);
    
  3. Maak een exemplaar van met behulp van VirtualOutgoingVideoStream het RawOutgoingVideoStreamOptions exemplaar dat u eerder hebt gemaakt.

    ScreenShareOutgoingVideoStream rawOutgoingVideoStream = new ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  4. Leg het videoframe op de volgende manier vast en verzend het.

    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();
        }
    }
    

Onbewerkte binnenkomende video

Met deze functie krijgt u toegang tot de videoframes in de IncomingVideoStream objecten om deze frames lokaal te bewerken

  1. Een exemplaar van IncomingVideoOptions die sets maken via JoinCallOptions instelling VideoStreamKind.RawIncoming

    IncomingVideoOptions incomingVideoOptions = new IncomingVideoOptions()
            .setStreamType(VideoStreamKind.RAW_INCOMING);
    
    JoinCallOptions joinCallOptions = new JoinCallOptions()
            .setIncomingVideoOptions(incomingVideoOptions);
    
  2. Zodra u een gedelegeerde voor het koppelen RemoteParticipant.VideoStreamStateChanged van gebeurtenissen ParticipantsUpdatedEventArgs ontvangt. Met deze gebeurtenis wordt de status van het IncomingVideoStream object geïnformeerd.

    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. Op het moment heeft VideoStreamState.Available de IncomingVideoStream status gedelegeerde voor het koppelen RawIncomingVideoStream.RawVideoFrameReceived van de status, zoals wordt weergegeven in de vorige stap. Deze gedelegeerde biedt de nieuwe RawVideoFrame objecten.

    private void OnVideoFrameReceived(RawVideoFrameReceivedEventArgs args)
    {
        // Render/Modify/Save the video frame
        RawVideoFrameBuffer videoFrame = (RawVideoFrameBuffer) args.getFrame();
    }
    

In deze quickstart leert u hoe u onbewerkte mediatoegang implementeert met behulp van de Azure Communication Services Calling SDK voor iOS.

De Azure Communication Services Calling SDK biedt API's waarmee apps hun eigen videoframes kunnen genereren om te verzenden naar externe deelnemers in een gesprek.

Deze quickstart bouwt voort op quickstart: 1:1 videogesprekken toevoegen aan uw app voor iOS.

RawAudio-toegang

Als u toegang krijgt tot onbewerkte audiomedia, hebt u toegang tot de audiostream van het binnenkomende gesprek, samen met de mogelijkheid om aangepaste uitgaande audiostreams tijdens een gesprek weer te geven en te verzenden.

Onbewerkte uitgaande audio verzenden

Maak een optiesobject waarin de onbewerkte stroomeigenschappen worden opgegeven die we willen verzenden.

    let outgoingAudioStreamOptions = RawOutgoingAudioStreamOptions()
    let properties = RawOutgoingAudioStreamProperties()
    properties.sampleRate = .hz44100
    properties.bufferDuration = .inMs20
    properties.channelMode = .mono
    properties.format = .pcm16Bit
    outgoingAudioStreamOptions.properties = properties

Maak een RawOutgoingAudioStream en voeg deze toe om deel te nemen aan gespreksopties en de stream wordt automatisch gestart wanneer de oproep is verbonden.

    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.

Stream koppelen aan een gesprek

U kunt de stream ook koppelen aan een bestaand Call exemplaar:


    call.startAudio(stream: self.rawOutgoingAudioStream) { error in 
        // Stream attached to `Call`.
    }

Onbewerkte voorbeelden verzenden

We kunnen alleen beginnen met het verzenden van gegevens zodra de stroomstatus is AudioStreamState.started. Om de statuswijziging van de audiostream te observeren, implementeren we de RawOutgoingAudioStreamDelegate. En stel deze in als de streamdelegeer.

    func rawOutgoingAudioStream(_ rawOutgoingAudioStream: RawOutgoingAudioStream,
                                didChangeState args: AudioStreamStateChangedEventArgs) {
        // When value is `AudioStreamState.started` we will be able to send audio samples.
    }

    self.rawOutgoingAudioStream.delegate = DelegateImplementer()

of op basis van sluiting gebruiken

    self.rawOutgoingAudioStream.events.onStateChanged = { args in
        // When value is `AudioStreamState.started` we will be able to send audio samples.
    }

Wanneer de stream is gestart, kunnen we beginnen met het verzenden van AVAudioPCMBuffer audiovoorbeelden naar het gesprek.

De audiobufferindeling moet overeenkomen met de opgegeven stroomeigenschappen.

    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()
        }
    }

Het is ook belangrijk om te onthouden dat u de audiostream in het huidige gespreksexemplaren Call stopt:


    call.stopAudio(stream: self.rawOutgoingAudioStream) { error in 
        // Stream detached from `Call` and stopped.
    }

Voorbeelden van microfoon vastleggen

Met behulp van Apple AVAudioEngine kunnen we microfoonframes vastleggen door te tikken op het invoerknooppunt van de audio-engine. En het vastleggen van de microfoongegevens en het gebruik van onbewerkte audiofunctionaliteit, we kunnen de audio verwerken voordat deze naar een gesprek wordt verzonden.

    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()
        }
    }

Notitie

De voorbeeldfrequentie van het invoerknooppunt van de audio-engine is standaard ingesteld op een >waarde van de gewenste samplefrequentie voor de gedeelde audiosessie. Tik dus niet in dat knooppunt met een andere waarde. We moeten er dus voor zorgen dat de samplefrequentie van de RawOutgoingStream eigenschappen overeenkomt met de waarde die we krijgen van tap in microfoonvoorbeelden of de tapbuffers converteren naar de indeling die overeenkomt met wat er wordt verwacht voor de uitgaande stream.

Met dit kleine voorbeeld hebben we geleerd hoe we de microfoongegevens AVAudioEngine kunnen vastleggen en deze voorbeelden naar een gesprek kunnen verzenden met behulp van een onbewerkte uitgaande audiofunctie.

Raw-audio ontvangen

We kunnen ook de voorbeelden van de gespreksaudiostream ontvangen alsof AVAudioPCMBuffer we de audio willen verwerken voordat ze worden afgespeeld.

Maak een RawIncomingAudioStreamOptions object waarin de onbewerkte stroomeigenschappen worden opgegeven die we willen ontvangen.

    let options = RawIncomingAudioStreamOptions()
    let properties = RawIncomingAudioStreamProperties()
    properties.format = .pcm16Bit
    properties.sampleRate = .hz44100
    properties.channelMode = .stereo
    options.properties = properties

Maak een RawOutgoingAudioStream en voeg deze toe om deel te nemen aan gespreksopties

    let options =  JoinCallOptions() // or StartCallOptions()
    let incomingAudioOptions = IncomingAudioOptions()

    self.rawIncomingStream = RawIncomingAudioStream(rawIncomingAudioStreamOptions: audioStreamOptions)
    incomingAudioOptions.stream = self.rawIncomingStream
    options.incomingAudioOptions = incomingAudioOptions

Of we kunnen de stream ook koppelen aan een bestaand Call exemplaar:


    call.startAudio(stream: self.rawIncomingStream) { error in 
        // Stream attached to `Call`.
    }

Voor het ontvangen van onbewerkte audiobuffer van de binnenkomende stream implementeert u het RawIncomingAudioStreamDelegatevolgende:

    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()

or

    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-toegang

Omdat de app de videoframes genereert, moet de app de Azure Communication Services Calling SDK informeren over de video-indelingen die de app kan genereren. Met deze informatie kan de Sdk voor aanroepen van Azure Communication Services de beste configuratie van de video-indeling kiezen voor de netwerkvoorwaarden op dat moment.

Virtuele video

Ondersteunde videoresoluties

Hoogte-breedteverhouding Oplossing Maximum 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. Maak een matrix van het gebruik van VideoFormat de VideoStream PixelFormat die door de SDK wordt ondersteund. Wanneer er meerdere indelingen beschikbaar zijn, heeft de volgorde van de indelingen in de lijst geen invloed op of geeft deze prioriteit aan welke wordt gebruikt. De criteria voor het selecteren van opmaak zijn gebaseerd op externe factoren, zoals netwerkbandbreedte.

    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. Maak RawOutgoingVideoStreamOptionsen stel opmaak in met het eerder gemaakte object.

    var rawOutgoingVideoStreamOptions = RawOutgoingVideoStreamOptions()
    rawOutgoingVideoStreamOptions.formats = videoStreamFormats
    
  3. Maak een exemplaar van met behulp van VirtualOutgoingVideoStream het RawOutgoingVideoStreamOptions exemplaar dat u eerder hebt gemaakt.

    var rawOutgoingVideoStream = VirtualOutgoingVideoStream(videoStreamOptions: rawOutgoingVideoStreamOptions)
    
  4. Implementeren voor de VirtualOutgoingVideoStreamDelegate gemachtigde. De didChangeFormat gebeurtenis informeert wanneer de VideoStreamFormat video is gewijzigd van een van de video-indelingen in de lijst.

    virtualOutgoingVideoStream.delegate = /* Attach delegate and implement didChangeFormat */
    
  5. Een exemplaar van de volgende helperklasse maken voor toegang tot CVPixelBuffer gegevens

    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. Een exemplaar van de volgende helperklasse maken om willekeurige RawVideoFrameBufferexemplaren te genereren met behulp van VideoStreamPixelFormat.rgba

    final class VideoFrameSender : NSObject
    {
        private var rawOutgoingVideoStream: RawOutgoingVideoStream
        private var frameIteratorThread: Thread
        private var stopFrameIterator: Bool = false
    
        public VideoFrameSender(rawOutgoingVideoStream: RawOutgoingVideoStream)
        {
            self.rawOutgoingVideoStream = rawOutgoingVideoStream
        }
    
        @objc private func VideoFrameIterator()
        {
            while !stopFrameIterator {
                if rawOutgoingVideoStream != nil &&
                   rawOutgoingVideoStream.format != nil &&
                   rawOutgoingVideoStream.state == .started {
                    SendRandomVideoFrameNV12()
                }
           }
        }
    
        public func SendRandomVideoFrameNV12() -> Void
        {
            let videoFrameBuffer = GenerateRandomVideoFrameBuffer()
    
            rawOutgoingVideoStream.send(frame: videoFrameBuffer) { error in
                /*Handle error if non-nil*/
            }
    
            let rate = 0.1 / rawOutgoingVideoStream.format.framesPerSecond
            let second: Float = 1000000
            usleep(useconds_t(rate * second))
        }
    
        private func GenerateRandomVideoFrameBuffer() -> RawVideoFrame
        {
            var cvPixelBuffer: CVPixelBuffer? = nil
            guard CVPixelBufferCreate(kCFAllocatorDefault,
                                    rawOutgoingVideoStream.format.width,
                                    rawOutgoingVideoStream.format.height,
                                    kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
                                    nil,
                                    &cvPixelBuffer) == kCVReturnSuccess else {
                fatalError()
            }
    
            GenerateRandomVideoFrameNV12(cvPixelBuffer: cvPixelBuffer!)
    
            CVPixelBufferUnlockBaseAddress(cvPixelBuffer!, .readOnly)
    
            let videoFrameBuffer = RawVideoFrameBuffer()
            videoFrameBuffer.buffer = cvPixelBuffer!
            videoFrameBuffer.streamFormat = rawOutgoingVideoStream.format
    
            return videoFrameBuffer
        }
    
       private func GenerateRandomVideoFrameNV12(cvPixelBuffer: CVPixelBuffer) {
            let w = rawOutgoingVideoStream.format.width
            let h = rawOutgoingVideoStream.format.height
    
            let bufferArrayList = BufferExtensions.getArrayBuffersUnsafe(cvPixelBuffer: cvPixelBuffer)
    
            guard bufferArrayList.count >= 2, let yArrayBuffer = bufferArrayList[0], let uvArrayBuffer = bufferArrayList[1] else {
                return
            }
    
            let yVal = Int32.random(in: 1..<255)
            let uvVal = Int32.random(in: 1..<255)
    
            for y in 0...h
            {
                for x in 0...w
                {
                    yArrayBuffer.storeBytes(of: yVal, toByteOffset: Int((y * w) + x), as: Int32.self)
                }
            }
    
            for y in 0...(h/2)
            {
                for x in 0...(w/2)
                {
                    uvArrayBuffer.storeBytes(of: uvVal, toByteOffset: Int((y * w) + x), as: Int32.self)
                }
            }
        }
    
        public func Start() {
            stopFrameIterator = false
            frameIteratorThread = Thread(target: self, selector: #selector(VideoFrameIterator), object: "VideoFrameSender")
            frameIteratorThread?.start()
        }
    
        public func Stop() {
            if frameIteratorThread != nil {
                stopFrameIterator = true
                frameIteratorThread?.cancel()
                frameIteratorThread = nil
            }
        }
    }
    
  7. Implementeren op de VirtualOutgoingVideoStreamDelegate. De didChangeState gebeurtenis informeert de status van de huidige stream. Verzend geen frames als de status niet gelijk is aan 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 Video

Omdat het Windows-systeem de frames genereert, moet u uw eigen voorgrondservice implementeren om de frames vast te leggen en te verzenden met behulp van de Api voor aanroepen van Azure Communication Services.

Ondersteunde videoresoluties

Hoogte-breedteverhouding Oplossing Maximum FPS
Iets Alles tot 1080p 30

Stappen voor het maken van een videostream voor het delen van een scherm

  1. Maak een matrix van het gebruik van VideoFormat de VideoStream PixelFormat die door de SDK wordt ondersteund. Wanneer er meerdere indelingen beschikbaar zijn, heeft de volgorde van de indelingen in de lijst geen invloed op of geeft deze prioriteit aan welke wordt gebruikt. De criteria voor het selecteren van opmaak zijn gebaseerd op externe factoren, zoals netwerkbandbreedte.

    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. Maak RawOutgoingVideoStreamOptionsen stel deze in VideoFormats met het eerder gemaakte object.

    var rawOutgoingVideoStreamOptions = RawOutgoingVideoStreamOptions()
    rawOutgoingVideoStreamOptions.formats = videoStreamFormats
    
  3. Maak een exemplaar van met behulp van VirtualOutgoingVideoStream het RawOutgoingVideoStreamOptions exemplaar dat u eerder hebt gemaakt.

    var rawOutgoingVideoStream = ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions)
    
  4. Leg het videoframe op de volgende manier vast en verzend het.

    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*/
        }
    }
    

Onbewerkte binnenkomende video

Met deze functie krijgt u toegang tot de videoframes in de IncomingVideoStream's om deze streamobjecten lokaal te bewerken

  1. Een exemplaar van IncomingVideoOptions die sets maken via JoinCallOptions instelling VideoStreamKind.RawIncoming

    var incomingVideoOptions = IncomingVideoOptions()
    incomingVideoOptions.streamType = VideoStreamKind.rawIncoming
    var joinCallOptions = JoinCallOptions()
    joinCallOptions.incomingVideoOptions = incomingVideoOptions
    
  2. Zodra u een gedelegeerde voor het koppelen RemoteParticipant.delegate.didChangedVideoStreamState van gebeurtenissen ParticipantsUpdatedEventArgs ontvangt. Met deze gebeurtenis wordt de status van de IncomingVideoStream objecten geïnformeerd.

    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. Op het moment heeft VideoStreamState.available de IncomingVideoStream status gedelegeerde voor het koppelen RawIncomingVideoStream.delegate.didReceivedRawVideoFrame van de status, zoals wordt weergegeven in de vorige stap. Deze gebeurtenis biedt de nieuwe RawVideoFrame objecten.

    func rawIncomingVideoStream(_ rawIncomingVideoStream: RawIncomingVideoStream, 
                                didRawVideoFrameReceived args: RawVideoFrameReceivedEventArgs) {
        /* Render/Modify/Save the video frame */
        let videoFrame = args.frame as! RawVideoFrameBuffer
    }
    

Als ontwikkelaar hebt u toegang tot de onbewerkte media voor binnenkomende en uitgaande audio, video en scherm delen tijdens een gesprek, zodat u audio-/video-inhoud kunt vastleggen, analyseren en verwerken. Toegang tot onbewerkte audio-, onbewerkte video- en onbewerkte schermshares aan de clientzijde van Azure Communication Services biedt ontwikkelaars een bijna onbeperkte mogelijkheid om audio, video en scherminhoud te bekijken en te bewerken die plaatsvindt binnen de Azure Communication Services Calling SDK. In deze quickstart leert u hoe u onbewerkte mediatoegang implementeert met behulp van de Azure Communication Services Calling SDK voor JavaScript.

Voorbeeld:

  • U kunt de audio-/videostream van het gesprek rechtstreeks op het oproepobject openen en aangepaste uitgaande audio-/videostreams verzenden tijdens het gesprek.
  • U kunt audio- en videostreams inspecteren om aangepaste AI-modellen uit te voeren voor analyse. Dergelijke modellen kunnen natuurlijke taalverwerking bevatten om gesprekken te analyseren of om realtime inzichten en suggesties te bieden om de productiviteit van agents te verhogen.
  • Organisaties kunnen audio- en videomediastreams gebruiken om sentiment te analyseren bij het bieden van virtuele zorg voor patiënten of om hulp op afstand te bieden tijdens videogesprekken die mixed reality gebruiken. Met deze mogelijkheid wordt een pad geopend voor ontwikkelaars om innovaties toe te passen om interactie-ervaringen te verbeteren.

Vereisten

Belangrijk

De voorbeelden hier zijn beschikbaar in 1.13.1 van de Calling SDK voor JavaScript. Zorg ervoor dat u die versie of nieuwer gebruikt wanneer u deze quickstart probeert.

Toegang tot onbewerkte audio

Als u toegang krijgt tot onbewerkte audiomedia, hebt u toegang tot de audiostream van het binnenkomende gesprek, samen met de mogelijkheid om aangepaste uitgaande audiostreams tijdens een gesprek weer te geven en te verzenden.

Toegang tot een binnenkomende onbewerkte audiostream

Gebruik de volgende code voor toegang tot de audiostream van een binnenkomend gesprek.

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);

Een gesprek plaatsen met een aangepaste audiostream

Gebruik de volgende code om een gesprek te starten met een aangepaste audiostream in plaats van het microfoonapparaat van een gebruiker te gebruiken.

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);

Overschakelen naar een aangepaste audiostream tijdens een gesprek

Gebruik de volgende code om een invoerapparaat over te schakelen naar een aangepaste audiostream in plaats van het microfoonapparaat van een gebruiker te gebruiken tijdens een gesprek.

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);

Een aangepaste audiostream stoppen

Gebruik de volgende code om te stoppen met het verzenden van een aangepaste audiostream nadat deze tijdens een gesprek is ingesteld.

call.stopAudio();

Onbewerkte video openen

Onbewerkte videomedia geven u het exemplaar van een MediaStream object. (Zie de JavaScript-documentatie voor meer informatie.) Onbewerkte videomedia bieden specifiek toegang tot het MediaStream object voor binnenkomende en uitgaande oproepen. Voor onbewerkte video kunt u dat object gebruiken om filters toe te passen met behulp van machine learning om frames van de video te verwerken.

Verwerkte onbewerkte uitgaande videoframes kunnen worden verzonden als een uitgaande video van de afzender. Verwerkte onbewerkte binnenkomende videoframes kunnen aan de ontvangerzijde worden weergegeven.

Een gesprek plaatsen met een aangepaste videostream

U hebt toegang tot de onbewerkte videostream voor een uitgaande oproep. U gebruikt MediaStream voor de uitgaande onbewerkte videostream om frames te verwerken met behulp van machine learning en om filters toe te passen. De verwerkte uitgaande video kan vervolgens worden verzonden als een videostream van een afzender.

In dit voorbeeld worden canvasgegevens naar een gebruiker verzonden als uitgaande video.

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);

Overschakelen naar een aangepaste videostream tijdens een gesprek

Gebruik de volgende code om een invoerapparaat over te schakelen naar een aangepaste videostream in plaats van het cameraapparaat van een gebruiker te gebruiken tijdens een gesprek.

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);

Een aangepaste videostream stoppen

Gebruik de volgende code om te stoppen met het verzenden van een aangepaste videostream nadat deze tijdens een gesprek is ingesteld.

// Stop video by passing the same `localVideoStream` instance that was used to start video
await call.stopVideo(localVideoStream);

Wanneer u overschakelt van een camera waarop aangepaste effecten zijn toegepast op een ander cameraapparaat, moet u eerst de video stoppen, de bron op de LocalVideoStreamen de video opnieuw starten.

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);
}

Toegang tot binnenkomende videostream van een externe deelnemer

U hebt toegang tot de onbewerkte videostream voor een inkomende oproep. U gebruikt MediaStream voor de binnenkomende onbewerkte videostream om frames te verwerken met behulp van machine learning en om filters toe te passen. De verwerkte binnenkomende video kan vervolgens worden weergegeven aan de ontvangerzijde.

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();

Belangrijk

Deze functie van Azure Communication Services is momenteel beschikbaar als preview-versie.

Preview-API's en SDK's worden aangeboden zonder een service level agreement. U wordt aangeraden deze niet te gebruiken voor productieworkloads. Sommige functies worden mogelijk niet ondersteund of hebben mogelijk beperkte mogelijkheden.

Raadpleeg aanvullende gebruiksvoorwaarden voor Microsoft Azure Previews voor meer informatie.

Onbewerkte toegang tot schermdeling is beschikbaar in openbare preview en is beschikbaar als onderdeel van versie 1.15.1-beta.1+.

Toegang tot onbewerkt scherm delen

Onbewerkte schermsharemedia bieden specifiek toegang tot het MediaStream object voor binnenkomende en uitgaande schermsharestreams. Voor het delen van onbewerkte schermen kunt u dat object gebruiken om filters toe te passen met behulp van machine learning om frames van de schermshare te verwerken.

Verwerkte frames voor onbewerkte schermshares kunnen worden verzonden als een uitgaande schermshare van de afzender. Verwerkte onbewerkte binnenkomende schermshareframes kunnen worden weergegeven aan de ontvangerzijde.

Opmerking: het verzenden van schermshares wordt alleen ondersteund in de bureaubladbrowser.

Scherm delen met een aangepaste stream voor schermshares

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'

Open de stream voor onbewerkte schermshares vanaf een scherm, browsertabblad of app en pas effecten toe op de stream

Hier volgt een voorbeeld van het toepassen van een zwart-wit effect op de stream voor het delen van onbewerkte schermen vanaf een scherm, browsertabblad of app. OPMERKING: Het canvascontextfilter = 'grayscale(1)' API wordt niet ondersteund in 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;

Stoppen met het verzenden van de stream voor schermshares

Gebruik de volgende code om te stoppen met het verzenden van een aangepaste stream voor schermshares nadat deze is ingesteld tijdens een gesprek.

// Stop sending raw screen sharing stream
await call.stopScreenSharing(localScreenSharingStream);

Toegang tot de stream voor binnenkomende schermshares van een externe deelnemer

U hebt toegang tot de stream voor onbewerkte schermshares van een externe deelnemer. U gebruikt MediaStream de stroom voor de binnenkomende onbewerkte schermshare om frames te verwerken met behulp van machine learning en om filters toe te passen. De verwerkte stream voor binnenkomende schermshares kan vervolgens worden weergegeven aan de ontvangerzijde.

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();

Volgende stappen

Raadpleeg voor meer informatie de volgende artikelen: