Freigeben über


Schnellstart: Hinzufügen von Zugriff auf unformatierte Medien zu Ihrer App

In diesem Schnellstart erfahren Sie, wie Sie den Zugriff auf unformatierte Medien mit dem Azure Communication Services-Calling SDK für Unity implementieren. Das Calling SDK von Azure Communication Services bietet APIs, mit denen Apps ihre eigenen Videoframes generieren können, um in einem Anruf von Remoteteilnehmer*innen unformatierte Videoframes zu senden oder zu rendern. Dieser Schnellstart basiert auf dem Schnellstart: Hinzufügen von 1:1-Videoanrufen zu Ihrer App für Unity.

RawVideo-Zugriff

Weil die App die Videoframes generiert, muss sie das Calling-SDK von Azure Communication Services über die Videoformate informieren, die die App generieren kann. Anhand dieser Informationen kann das Azure Communication Services Calling SDK jederzeit die beste Videoformatkonfiguration unter den Netzwerkbedingungen auswählen.

Virtuelles Video

Unterstützte Videoauflösungen

Seitenverhältnis Lösung Maximale 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. Führen Sie die hier Schnellstart: Hinzufügen von 1:1-Videoanrufen zu Ihrer App beschriebenen Schritte aus, um ein Unity-Spiel zu erstellen. Das Ziel besteht darin, ein CallAgent-Objekt abzurufen, das zum Starten des Anrufs bereit ist. Den fertigen Code für diese Schnellstartanleitung finden Sie auf GitHub.

  2. Erstellen Sie ein Array von VideoFormat mithilfe von VideoStreamPixelFormat, das das SDK unterstützt. Wenn mehrere Formate verfügbar sind, wirkt sich die Reihenfolge der Formate in der Liste nicht auf die Auswahl oder Priorisierung des Formats, das verwendet wird, aus. Die Kriterien für die Formatauswahl basieren auf externen Faktoren wie Netzwerkbandbreite.

    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. Erstellen Sie RawOutgoingVideoStreamOptions, und legen Sie Formats mit dem zuvor erstellten Objekt fest.

    var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions
    {
        Formats = videoStreamFormats
    };
    
  4. Erstellen Sie mithilfe der RawOutgoingVideoStreamOptions-Instanz, die Sie zuvor erstellt haben, eine Instanz von VirtualOutgoingVideoStream.

    var rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  5. Abonnieren Sie den Delegat RawOutgoingVideoStream.FormatChanged. Dieses Ereignis informiert jedes Mal, wenn VideoStreamFormat von einem der in der Liste angegebenen Videoformate geändert wurde.

    rawOutgoingVideoStream.FormatChanged += (object sender, VideoStreamFormatChangedEventArgs args)
    {
        VideoStreamFormat videoStreamFormat = args.Format;
    }
    
  6. Abonnieren Sie den Delegat RawOutgoingVideoStream.StateChanged. Dieses Ereignis informiert jedes Mal, wenn sich der State geändert hat.

    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. Behandeln Sie unformatierte ausgehende Videostream-Statustransaktionen wie „Start“ und „Stopp“, und beginnen Sie, benutzerdefinierte Videoframes zu generieren, oder den Algorithmus zum Generieren von Frames anzuhalten.

    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 ist ein Beispiel für den Generator für ausgehende Videoframes:

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

    Hinweis

    Der unsafe-Modifizierer wird für diese Methode verwendet, da für NativeBuffer der Zugriff auf native Arbeitsspeicherressourcen erforderlich ist. Daher muss auch die Option Allow unsafe im Unity-Editor aktiviert sein.

  8. Ebenso können wir eingehende Videoframes als Reaktion auf das StateChanged-Ereignis im Videostream behandeln.

    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. Es wird dringend empfohlen, sowohl eingehende als auch ausgehende Videoframes über einen Puffermechanismus zu verwalten, um eine Überlastung der MonoBehaviour.Update()-Rückrufmethode zu vermeiden, die leicht gehalten werden sollte und CPU- oder netzwerklastige Aufgaben vermeiden und eine reibungslosere Videoerfahrung gewährleisten sollte. Der Entscheid, welche Elemente dieser optionale Optimierung in ihren Szenarien am besten funktionieren, bleibt Entwickler*innen überlassen.

    Hier ist ein Beispiel dafür, wie die eingehenden Frames in einer Unity-VideoTexture gerendert werden können, indem Graphics.Blit aus einer internen Warteschlange angerufen wird:

    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 dieser Schnellstartanleitung erfahren Sie, wie Sie den Zugriff auf unformatierte Medien mithilfe des Calling-SDK von Azure Communication Services für Windows implementieren. Das Azure Communication Services Calling SDK bietet APIs, mit denen Apps eigene Videoframes generieren können, die in einem Anruf an Remoteteilnehmer*innen gesendet werden können. Diese Schnellstartanleitung baut auf Schnellstart: Hinzufügen von 1:1-Videoanrufen zu Ihrer App für Windows auf.

RawAudio-Zugriff

Der Zugriff auf unformatierte Audiomedien bietet Zugriff auf Audiostreams in eingehenden Anrufen und die Möglichkeit, während eines Anrufs benutzerdefinierte ausgehende Audiostreams anzuzeigen und zu senden.

Senden von unformatierten ausgehenden Audiodaten

Erstellen Sie ein Optionsobjekt, das die unformatierten Streameigenschaften angibt, die wir senden möchten.

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

Erstellen Sie ein RawOutgoingAudioStream-Objekt, und fügen Sie es an, um Anrufoptionen zu verbinden. Der Stream wird automatisch gestartet, wenn der Anruf verbunden ist.

    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.

Anfügen eines Streams an einen Anruf

Alternativ können Sie den Stream auch an eine vorhandene Call-Instanz anfügen:

    await call.StartAudio(rawOutgoingAudioStream);

Starten des Sendens von unformatierten Beispielen

Wir können erst mit dem Senden von Daten beginnen, wenn der Streamstatus AudioStreamState.Started lautet. Um die Änderung des Audiostreamstatus zu beobachten, fügen Sie dem OnStateChangedListener-Ereignis einen Listener hinzu.

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

Wenn der Stream gestartet wurde, können wir damit beginnen, MemoryBuffer-Audiobeispiele an den Anruf zu senden. Das Audiopufferformat sollte mit den angegebenen Streameigenschaften übereinstimmen.

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

Empfangen von unformatierten eingehenden Audiodaten

Wir können auch die Anrufaudiostreambeispiele als MemoryBuffer empfangen, wenn wir den Anrufaudiostream vor der Wiedergabe verarbeiten möchten. Erstellen Sie ein RawIncomingAudioStreamOptions-Objekt, das die unformatierten Streameigenschaften angibt, die wir empfangen möchten.

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

Erstellen Sie ein RawIncomingAudioStream-Objekt, und fügen Sie es an, um Anrufoptionen zu verbinden

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

Alternativ können wir den Stream auch an eine vorhandene Call-Instanz anfügen:

    await call.startAudio(context, rawIncomingAudioStream);

Um damit zu beginnen, unformatierte Audiopuffer vom eingehenden Stream zu empfangen, fügen Sie dem eingehenden Streamstatus Listener hinzu, und puffern Sie empfangene Ereignisse.

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

Weil die App die Videoframes generiert, muss sie das Calling-SDK von Azure Communication Services über die Videoformate informieren, die die App generieren kann. Anhand dieser Informationen kann das Azure Communication Services Calling SDK jederzeit die beste Videoformatkonfiguration unter den Netzwerkbedingungen auswählen.

Virtuelles Video

Unterstützte Videoauflösungen

Seitenverhältnis Lösung Maximale 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. Erstellen Sie ein Array von VideoFormat mithilfe von VideoStreamPixelFormat, das das SDK unterstützt. Wenn mehrere Formate verfügbar sind, wirkt sich die Reihenfolge der Formate in der Liste nicht auf die Auswahl oder Priorisierung des Formats, das verwendet wird, aus. Die Kriterien für die Formatauswahl basieren auf externen Faktoren wie Netzwerkbandbreite.

    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. Erstellen Sie RawOutgoingVideoStreamOptions, und legen Sie Formats mit dem zuvor erstellten Objekt fest.

    var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions
    {
        Formats = videoStreamFormats
    };
    
  3. Erstellen Sie mithilfe der RawOutgoingVideoStreamOptions-Instanz, die Sie zuvor erstellt haben, eine Instanz von VirtualOutgoingVideoStream.

    var rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  4. Abonnieren Sie den Delegat RawOutgoingVideoStream.FormatChanged. Dieses Ereignis informiert jedes Mal, wenn VideoStreamFormat von einem der in der Liste angegebenen Videoformate geändert wurde.

    rawOutgoingVideoStream.FormatChanged += (object sender, VideoStreamFormatChangedEventArgs args)
    {
        VideoStreamFormat videoStreamFormat = args.Format;
    }
    
  5. Erstellen einer Instanz der folgenden Hilfsklasse für den Zugriff auf die Pufferdaten

    [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. Erstellen einer Instanz der folgenden Hilfsklasse, um ein zufälliges RawVideoFrame-Objekt mithilfe von VideoStreamPixelFormat.Rgba zu generieren

    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. Abonnieren Sie den Delegat VideoStream.StateChanged. Dieses Ereignis informiert den Status des aktuellen Streams. Senden Sie nur dann Frames, wenn der Status VideoStreamState.Started lautet.

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

Bildschirmfreigabevideo

Weil das Windows-System die Frames generiert, müssen Sie einen eigenen Vordergrunddienst implementieren, um die Frames aufzuzeichnen und sie über die Azure Communication Services Calling-API zu senden.

Unterstützte Videoauflösungen

Seitenverhältnis Lösung Maximale FPS
Irgendetwas Alles bis zu 1080p 30

Schritte zum Erstellen eines Videostreams für die Bildschirmfreigabe

  1. Erstellen Sie ein Array von VideoFormat mithilfe von VideoStreamPixelFormat, das das SDK unterstützt. Wenn mehrere Formate verfügbar sind, wirkt sich die Reihenfolge der Formate in der Liste nicht auf die Auswahl oder Priorisierung des Formats, das verwendet wird, aus. Die Kriterien für die Formatauswahl basieren auf externen Faktoren wie Netzwerkbandbreite.
    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. Erstellen Sie RawOutgoingVideoStreamOptions, und legen Sie VideoFormats mit dem zuvor erstellten Objekt fest.
    var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions
    {
        Formats = videoStreamFormats
    };
    
  3. Erstellen Sie mithilfe der RawOutgoingVideoStreamOptions-Instanz, die Sie zuvor erstellt haben, eine Instanz von VirtualOutgoingVideoStream.
    var rawOutgoingVideoStream = new ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  4. Erfassen und senden Sie den Videoframe auf die folgende Weise.
    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;
        }
    }
    

Unformatierte eingehende Videodaten

Mit diesem Feature können Sie auf die Videoframes innerhalb des IncomingVideoStream-Objekts zugreifen, um diese Streams lokal zu bearbeiten

  1. Erstellen einer Instanz von IncomingVideoOptions, die über JoinCallOptions die Einstellung VideoStreamKind.RawIncoming festlegt
    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. Sobald Sie ein ParticipantsUpdatedEventArgs-Ereignis empfangen haben, fügen Sie den RemoteParticipant.VideoStreamStateChanged-Delegaten an. Dieses Ereignis informiert den Status der IncomingVideoStream-Objekte.
    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. Wenn der Status für IncomingVideoStreamVideoStreamState.Available lautet, fügen sie den RawIncomingVideoStream.RawVideoFrameReceived-Delegaten, wie im vorherigen Schritt angezeigt, an. Dadurch werden die neuen RawVideoFrame-Objekte bereitgestellt.
    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 dieser Schnellstartanleitung erfahren Sie, wie Sie den Zugriff auf unformatierte Medien mithilfe des Calling-SDK von Azure Communication Services für Android implementieren.

Das Azure Communication Services Calling SDK bietet APIs, mit denen Apps eigene Videoframes generieren können, die in einem Anruf an Remoteteilnehmer*innen gesendet werden können.

Diese Schnellstartanleitung baut auf Schnellstart: Hinzufügen von 1:1-Videoanrufen zu Ihrer App für Android auf.

RawAudio-Zugriff

Der Zugriff auf unformatierte Audiomedien bietet Zugriff auf eingehende Audiostreams des Anrufs und die Möglichkeit, während eines Anrufs benutzerdefinierte ausgehende Audiostreams anzuzeigen und zu senden.

Senden von unformatierten ausgehenden Audiodaten

Erstellen Sie ein Optionsobjekt, das die unformatierten Streameigenschaften angibt, die wir senden möchten.

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

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

Erstellen Sie ein RawOutgoingAudioStream-Objekt, und fügen Sie es an, um Anrufoptionen zu verbinden. Der Stream wird automatisch gestartet, wenn der Anruf verbunden ist.

    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.

Anfügen eines Streams an einen Anruf

Alternativ können Sie den Stream auch an eine vorhandene Call-Instanz anfügen:

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

Starten des Sendens von unformatierten Beispielen

Wir können erst mit dem Senden von Daten beginnen, wenn der Streamstatus AudioStreamState.STARTED lautet. Um die Änderung des Audiostreamstatus zu beobachten, fügen Sie dem OnStateChangedListener-Ereignis einen Listener hinzu.

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

    rawOutgoingAudioStream.addOnStateChangedListener(this::onStateChanged)

Wenn der Stream gestartet wurde, können wir damit beginnen, java.nio.ByteBuffer-Audiobeispiele an den Anruf zu senden.

Das Audiopufferformat sollte mit den angegebenen Streameigenschaften übereinstimmen.

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

Empfangen von unformatierten eingehenden Audiodaten

Wir können auch die Anrufaudiostreambeispiele als java.nio.ByteBuffer empfangen, wenn wir die Audiodaten vor der Wiedergabe verarbeiten möchten.

Erstellen Sie ein RawIncomingAudioStreamOptions-Objekt, das die unformatierten Streameigenschaften angibt, die wir empfangen möchten.

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

Erstellen Sie ein RawIncomingAudioStream-Objekt, und fügen Sie es an, um Anrufoptionen zu verbinden

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

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

Alternativ können wir den Stream auch an eine vorhandene Call-Instanz anfügen:


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

Um damit zu beginnen, unformatierte Audiopuffer vom eingehenden Stream zu empfangen, fügen Sie dem eingehenden Streamstatus Listener hinzu, und puffern Sie empfangene Ereignisse.

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

Denken Sie auch daran, den Audiostream in der aktuellen Anrufinstanz Call zu beenden:


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

RawVideo-Zugriff

Weil die App die Videoframes generiert, muss sie das Calling-SDK von Azure Communication Services über die Videoformate informieren, die die App generieren kann. Anhand dieser Informationen kann das Azure Communication Services Calling SDK jederzeit die beste Videoformatkonfiguration unter den Netzwerkbedingungen auswählen.

Virtuelles Video

Unterstützte Videoauflösungen

Seitenverhältnis Lösung Maximale 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. Erstellen Sie ein Array von VideoFormat mithilfe von VideoStreamPixelFormat, das das SDK unterstützt.

    Wenn mehrere Formate verfügbar sind, wirkt sich die Reihenfolge der Formate in der Liste nicht auf die Auswahl oder Priorisierung des Formats, das verwendet wird, aus. Die Kriterien für die Formatauswahl basieren auf externen Faktoren wie Netzwerkbandbreite.

    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. Erstellen Sie RawOutgoingVideoStreamOptions, und legen Sie Formats mit dem zuvor erstellten Objekt fest.

    RawOutgoingVideoStreamOptions rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions();
    rawOutgoingVideoStreamOptions.setFormats(videoStreamFormats);
    
  3. Erstellen Sie mithilfe der RawOutgoingVideoStreamOptions-Instanz, die Sie zuvor erstellt haben, eine Instanz von VirtualOutgoingVideoStream.

    VirtualOutgoingVideoStream rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  4. Abonnieren Sie den Delegat RawOutgoingVideoStream.addOnFormatChangedListener. Dieses Ereignis informiert jedes Mal, wenn VideoStreamFormat von einem der in der Liste angegebenen Videoformate geändert wurde.

    virtualOutgoingVideoStream.addOnFormatChangedListener((VideoStreamFormatChangedEvent args) -> 
    {
        VideoStreamFormat videoStreamFormat = args.Format;
    });
    
  5. Erstellen einer Instanz der folgenden Hilfsklasse, um ein zufälliges RawVideoFrame-Objekt mithilfe von VideoStreamPixelFormat.RGBA zu generieren

    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. Abonnieren Sie den Delegat VideoStream.addOnStateChangedListener. Dieser Delegat informiert den Status des aktuellen Streams. Senden Sie nur dann Frames, wenn der Status VideoStreamState.STARTED lautet.

    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

Weil das Windows-System die Frames generiert, müssen Sie einen eigenen Vordergrunddienst implementieren, um die Frames aufzuzeichnen und sie über die Azure Communication Services Calling-API zu senden.

Unterstützte Videoauflösungen

Seitenverhältnis Lösung Maximale FPS
Irgendetwas Alles bis zu 1080p 30

Schritte zum Erstellen eines Videostreams für die Bildschirmfreigabe

  1. Erstellen Sie ein Array von VideoFormat mithilfe von VideoStreamPixelFormat, das das SDK unterstützt.

    Wenn mehrere Formate verfügbar sind, wirkt sich die Reihenfolge der Formate in der Liste nicht auf die Auswahl oder Priorisierung des Formats, das verwendet wird, aus. Die Kriterien für die Formatauswahl basieren auf externen Faktoren wie Netzwerkbandbreite.

    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. Erstellen Sie RawOutgoingVideoStreamOptions, und legen Sie VideoFormats mit dem zuvor erstellten Objekt fest.

    RawOutgoingVideoStreamOptions rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions();
    rawOutgoingVideoStreamOptions.setFormats(videoStreamFormats);
    
  3. Erstellen Sie mithilfe der RawOutgoingVideoStreamOptions-Instanz, die Sie zuvor erstellt haben, eine Instanz von VirtualOutgoingVideoStream.

    ScreenShareOutgoingVideoStream rawOutgoingVideoStream = new ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  4. Erfassen und senden Sie den Videoframe auf die folgende Weise.

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

Unformatierte eingehende Videodaten

Mit diesem Feature können Sie auf die Videoframes innerhalb des IncomingVideoStream-Objekts zugreifen, um diese Frames lokal zu bearbeiten

  1. Erstellen einer Instanz von IncomingVideoOptions, die über JoinCallOptions die Einstellung VideoStreamKind.RawIncoming festlegt

    IncomingVideoOptions incomingVideoOptions = new IncomingVideoOptions()
            .setStreamType(VideoStreamKind.RAW_INCOMING);
    
    JoinCallOptions joinCallOptions = new JoinCallOptions()
            .setIncomingVideoOptions(incomingVideoOptions);
    
  2. Sobald Sie ein ParticipantsUpdatedEventArgs-Ereignis empfangen haben, fügen Sie den RemoteParticipant.VideoStreamStateChanged-Delegaten an. Dieses Ereignis informiert den Status des IncomingVideoStream-Objekts.

    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. Wenn der Status für IncomingVideoStreamVideoStreamState.Available lautet, fügen sie den RawIncomingVideoStream.RawVideoFrameReceived-Delegaten, wie im vorherigen Schritt angezeigt, an. Dieser Delegat stellt die neuen RawVideoFrame-Objekte bereit.

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

In dieser Schnellstartanleitung erfahren Sie, wie Sie den Zugriff auf unformatierte Medien mithilfe des Calling-SDK von Azure Communication Services für iOS implementieren.

Das Azure Communication Services Calling SDK bietet APIs, mit denen Apps eigene Videoframes generieren können, die in einem Anruf an Remoteteilnehmer*innen gesendet werden können.

Diese Schnellstartanleitung baut auf Schnellstart: Hinzufügen von 1:1-Videoanrufen zu Ihrer App für iOS auf.

RawAudio-Zugriff

Der Zugriff auf unformatierte Audiomedien bietet Zugriff auf Audiostreams in eingehenden Anrufen und die Möglichkeit, während eines Anrufs benutzerdefinierte ausgehende Audiostreams anzuzeigen und zu senden.

Senden von unformatierten ausgehenden Audiodaten

Erstellen Sie ein Optionsobjekt, das die unformatierten Streameigenschaften angibt, die wir senden möchten.

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

Erstellen Sie ein RawOutgoingAudioStream-Objekt, und fügen Sie es an, um Anrufoptionen zu verbinden. Der Stream wird automatisch gestartet, wenn der Anruf verbunden ist.

    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.

Anfügen eines Streams an einen Anruf

Alternativ können Sie den Stream auch an eine vorhandene Call-Instanz anfügen:


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

Starten des Sendens von unformatierten Beispielen

Wir können erst mit dem Senden von Daten beginnen, wenn der Streamstatus AudioStreamState.started lautet. Um die Änderung des Audiostreamstatus zu beobachten, implementieren wir den RawOutgoingAudioStreamDelegate. Und legen Sie ihn als Streamdelegat fest.

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

    self.rawOutgoingAudioStream.delegate = DelegateImplementer()

oder verwenden Sie den Abschluss basierend auf

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

Wenn der Stream gestartet wurde, können wir damit beginnen, AVAudioPCMBuffer-Audiobeispiele an den Anruf zu senden.

Das Audiopufferformat sollte mit den angegebenen Streameigenschaften übereinstimmen.

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

Denken Sie auch daran, den Audiostream in der aktuellen Anrufinstanz Call zu beenden:


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

Erfassen von Mikrofonbeispielen

Durch die Verwendung von Apples AVAudioEngine können wir Mikrofonframes erfassen, indem wir den Audio-Engine-Eingabeknoten nutzen. Durch die Erfassung der Mikrofondaten und die Verwendung von unformatierten Audiofunktionen können wir die Audiodaten vor dem Senden an einen Anruf verarbeiten.

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

Hinweis

Die Abtastrate des Audio-Engine-Eingabeknotens ist standardmäßig auf einen >-Wert der bevorzugten Abtastrate für die freigegebene Audiositzung festgelegt. Daher können wir TAP in diesem Knoten nicht mithilfe eines anderen Werts installieren. Daher müssen wir sicherstellen, dass die Abtastrate der RawOutgoingStream-Eigenschaften der Abtastrate entspricht, die wir erhalten, wenn wir Mikrofonbeispiele nutzen oder die TAP-Puffer in das Format konvertieren, das dem entspricht, was für den ausgehenden Stream erwartet wird.

In diesem kleinen Beispiel haben wir gelernt, wie wir die AVAudioEngine-Mikrofondaten erfassen und diese Beispiele mithilfe des Features für ausgehende unformatierte Audiodaten an einen Anruf senden können.

Empfangen von unformatierten eingehenden Audiodaten

Wir können auch die Anrufaudiostreambeispiele als AVAudioPCMBuffer empfangen, wenn wir die Audiodaten vor der Wiedergabe verarbeiten möchten.

Erstellen Sie ein RawIncomingAudioStreamOptions-Objekt, das die unformatierten Streameigenschaften angibt, die wir empfangen möchten.

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

Erstellen Sie ein RawOutgoingAudioStream-Objekt, und fügen Sie es an, um Anrufoptionen zu verbinden

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

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

Alternativ können wir den Stream auch an eine vorhandene Call-Instanz anfügen:


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

Um damit zu beginnen, unformatierten Audiopuffer vom eingehenden Stream zu empfangen, implementieren Sie den RawIncomingAudioStreamDelegate:

    class RawIncomingReceiver: NSObject, RawIncomingAudioStreamDelegate {
        func rawIncomingAudioStream(_ rawIncomingAudioStream: RawIncomingAudioStream,
                                    didChangeState args: AudioStreamStateChangedEventArgs) {
            // To be notified when stream started and stopped.
        }
        
        func rawIncomingAudioStream(_ rawIncomingAudioStream: RawIncomingAudioStream,
                                    mixedAudioBufferReceived args: IncomingMixedAudioEventArgs) {
            // Receive raw audio buffers(AVAudioPCMBuffer) and process using AVAudioEngine API's.
        }
    }

    self.rawIncomingStream.delegate = RawIncomingReceiver()

oder

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

Weil die App die Videoframes generiert, muss sie das Calling-SDK von Azure Communication Services über die Videoformate informieren, die die App generieren kann. Anhand dieser Informationen kann das Azure Communication Services Calling SDK jederzeit die beste Videoformatkonfiguration unter den Netzwerkbedingungen auswählen.

Virtuelles Video

Unterstützte Videoauflösungen

Seitenverhältnis Lösung Maximale 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. Erstellen Sie ein Array von VideoFormat mithilfe von VideoStreamPixelFormat, das das SDK unterstützt. Wenn mehrere Formate verfügbar sind, wirkt sich die Reihenfolge der Formate in der Liste nicht auf die Auswahl oder Priorisierung des Formats, das verwendet wird, aus. Die Kriterien für die Formatauswahl basieren auf externen Faktoren wie Netzwerkbandbreite.

    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. Erstellen Sie ein RawOutgoingVideoStreamOptions-Objekt, und legen Sie Formate mit dem zuvor erstellten Objekt fest.

    var rawOutgoingVideoStreamOptions = RawOutgoingVideoStreamOptions()
    rawOutgoingVideoStreamOptions.formats = videoStreamFormats
    
  3. Erstellen Sie mithilfe der RawOutgoingVideoStreamOptions-Instanz, die Sie zuvor erstellt haben, eine Instanz von VirtualOutgoingVideoStream.

    var rawOutgoingVideoStream = VirtualOutgoingVideoStream(videoStreamOptions: rawOutgoingVideoStreamOptions)
    
  4. Implementieren Sie den VirtualOutgoingVideoStreamDelegate-Delegaten. Das didChangeFormat-Ereignis informiert jedes Mal, wenn VideoStreamFormat von einem der in der Liste angegebenen Videoformate geändert wurde.

    virtualOutgoingVideoStream.delegate = /* Attach delegate and implement didChangeFormat */
    
  5. Erstellen einer Instanz der folgenden Hilfsklasse für den Zugriff auf CVPixelBuffer-Daten

    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. Erstellen einer Instanz der folgenden Hilfsklasse, um ein zufälliges RawVideoFrameBuffer-Objekt mithilfe von VideoStreamPixelFormat.rgba zu generieren

    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. Implementieren Sie den VirtualOutgoingVideoStreamDelegate. Das didChangeState-Ereignis informiert den Status des aktuellen Streams. Senden Sie nur dann Frames, wenn der Status VideoStreamState.started lautet.

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

Weil das Windows-System die Frames generiert, müssen Sie einen eigenen Vordergrunddienst implementieren, um die Frames aufzuzeichnen und sie über die Azure Communication Services Calling-API zu senden.

Unterstützte Videoauflösungen

Seitenverhältnis Lösung Maximale FPS
Irgendetwas Alles bis zu 1080p 30

Schritte zum Erstellen eines Videostreams für die Bildschirmfreigabe

  1. Erstellen Sie ein Array von VideoFormat mithilfe von VideoStreamPixelFormat, das das SDK unterstützt. Wenn mehrere Formate verfügbar sind, wirkt sich die Reihenfolge der Formate in der Liste nicht auf die Auswahl oder Priorisierung des Formats, das verwendet wird, aus. Die Kriterien für die Formatauswahl basieren auf externen Faktoren wie Netzwerkbandbreite.

    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. Erstellen Sie RawOutgoingVideoStreamOptions, und legen Sie VideoFormats mit dem zuvor erstellten Objekt fest.

    var rawOutgoingVideoStreamOptions = RawOutgoingVideoStreamOptions()
    rawOutgoingVideoStreamOptions.formats = videoStreamFormats
    
  3. Erstellen Sie mithilfe der RawOutgoingVideoStreamOptions-Instanz, die Sie zuvor erstellt haben, eine Instanz von VirtualOutgoingVideoStream.

    var rawOutgoingVideoStream = ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions)
    
  4. Erfassen und senden Sie den Videoframe auf die folgende Weise.

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

Unformatierte eingehende Videodaten

Mit diesem Feature können Sie auf die Videoframes innerhalb des IncomingVideoStream-Objekts zugreifen, um diese Streamobjekte lokal zu bearbeiten

  1. Erstellen einer Instanz von IncomingVideoOptions, die über JoinCallOptions die Einstellung VideoStreamKind.RawIncoming festlegt

    var incomingVideoOptions = IncomingVideoOptions()
    incomingVideoOptions.streamType = VideoStreamKind.rawIncoming
    var joinCallOptions = JoinCallOptions()
    joinCallOptions.incomingVideoOptions = incomingVideoOptions
    
  2. Sobald Sie ein ParticipantsUpdatedEventArgs-Ereignis empfangen haben, fügen Sie den RemoteParticipant.delegate.didChangedVideoStreamState-Delegaten an. Dieses Ereignis informiert den Status der IncomingVideoStream-Objekte.

    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. Wenn der Status für IncomingVideoStreamVideoStreamState.available lautet, fügen sie den RawIncomingVideoStream.delegate.didReceivedRawVideoFrame-Delegaten, wie im vorherigen Schritt angezeigt, an. Dieses Ereignis stellt die neuen RawVideoFrame-Objekte bereit.

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

Als Entwickler können Sie während eines Anrufs auf die Rohmedien für eingehende und ausgehende Audio-, Video- und Bildschirmfreigabeinhalte zugreifen, sodass Sie Audio-/Videoinhalte erfassen, analysieren und verarbeiten können. Durch den Zugriff auf die clientseitigen unformatierten Audio- und Videodaten von Azure Communication Services haben Entwickler praktisch unbegrenzte Möglichkeiten, Audio- und Videoinhalte, die innerhalb des Azure Communication Services Calling SDK entstehen, anzuzeigen und zu bearbeiten. In diesem Schnellstart erfahren Sie, wie Sie den Zugriff auf unformatierte Medien mithilfe des Azure Communication Services Calling SDK für JavaScript implementieren.

Beispiel:

  • Sie können direkt über das Anrufobjekt auf den eingehenden Anrufvideostream zugreifen und während des Anrufs einen benutzerdefinierten ausgehenden Videostream senden.
  • Sie können beispielsweise Audio- und Videostreams untersuchen, um benutzerdefinierte KI-Modelle für die Analyse auszuführen. Solche Modelle können die Verarbeitung natürlicher Sprache umfassen, um Unterhaltungen zu analysieren oder Echtzeiterkenntnisse und Vorschläge zu liefern, um die Agentproduktivität zu steigern.
  • Organisationen können Audio- und Videomedienstreams verwenden, um die Stimmung bei der virtuellen Versorgung von Patienten zu analysieren, oder um in Videoanrufen mit Mixed-Reality-Funktionen Remote-Unterstützung zu leisten. Diese Funktionalität eröffnet Entwicklern auch einen Weg, Innovationen zur Verbesserung der Interaktionserfahrungen anzuwenden.

Voraussetzungen

Wichtig

Die hier aufgeführten Beispiele sind in Version 1.13.1 des Calling SDK für JavaScript verfügbar. Stellen Sie sicher, dass Sie diese oder eine neuere Version verwenden, wenn Sie diese Schnellstartanleitung ausprobieren.

Zugreifen auf Rohaudiodaten

Der Zugriff auf unformatierte Audiomedien bietet Zugriff auf Audiostreams in eingehenden Anrufen und die Möglichkeit, während eines Anrufs benutzerdefinierte ausgehende Audiostreams anzuzeigen und zu senden.

Zugreifen auf einen eingehenden unformatierten Audiostream

Verwenden Sie den folgenden Code, um auf den Audiostream eines eingehenden Anrufs zuzugreifen.

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

Tätigen eines Anrufs mit einem benutzerdefinierten Audiostream

Verwenden Sie den folgenden Code, um einen Anruf mit einem benutzerdefinierten Audiostream zu starten, anstatt das Mikrofongerät der Benutzerin/des Benutzers zu verwenden.

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

Umschalten zu einem benutzerdefinierten Audiostream während eines Anrufs

Verwenden Sie den folgenden Code, um als Eingabegerät einen benutzerdefinierten Audiostream einzusetzen, anstatt das Mikrofongerät der Benutzerin/des Benutzers während eines Anrufs zu nutzen.

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

Beenden eines benutzerdefinierten Audiostreams

Verwenden Sie den folgenden Code, um das Senden eines benutzerdefinierten Audiostreams zu beenden, nachdem er während eines Anrufs festgelegt wurde.

call.stopAudio();

Zugreifen auf unformatierte Videodaten

Unformatierte Videomedien bieten die Instanz eines MediaStream-Objekts. (Weitere Informationen finden Sie in der JavaScript-Dokumentation.) Unformatierte Videomedien gewähren speziell Zugriff auf das MediaStream-Objekt für eingehende und ausgehende Aufrufe. Bei Rohvideodaten können Sie dieses Objekt verwenden, um mithilfe von maschinellem Lernen Filter anzuwenden und die Frames des Videos verarbeiten.

Verarbeitete ausgehende Rohvideoframes können als ausgehendes Video des Senders gesendet werden. Verarbeitete eingehende Rohvideoframes können auf der Empfängerseite gerendert werden.

Tätigen eines Anrufs mit einem benutzerdefinierten Videostream

Sie können auf den unformatierten Videostream für einen ausgehenden Anruf zugreifen. Sie verwenden MediaStream für den ausgehenden unformatierten Videostream, um Frames mithilfe von maschinellem Lernen zu verarbeiten und Filter anzuwenden. Die verarbeiteten ausgehenden Videodaten kann dann als Absendervideostream gesendet werden.

In diesem Beispiel werden Canvasdaten als ausgehende Videodaten an einen Benutzer gesendet.

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

Umschalten zu einem benutzerdefinierten Videostream während eines Anrufs

Verwenden Sie den folgenden Code, um als Eingabegerät einen benutzerdefinierten Videostream einzusetzen, anstatt das Kameragerät der Benutzerin/des Benutzers während eines Anrufs zu nutzen.

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

Beenden eines benutzerdefinierten Videostreams

Verwenden Sie den folgenden Code, um das Senden eines benutzerdefinierten Videostreams zu beenden, nachdem er während eines Anrufs festgelegt wurde.

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

Wenn Sie von einer Kamera wechseln, die benutzerdefinierte Effekte auf ein anderes Kameragerät angewendet hat, beenden Sie zunächst das Video, dann schalten Sie die Quelle auf LocalVideoStream, und starten Sie das Video erneut.

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

Zugreifen auf den eingehenden Videostream durch einen Remoteteilnehmer

Sie können auf den unformatierten Videostream für einen eingehenden Anruf zugreifen. Sie verwenden MediaStream für den eingehenden unformatierten Videostream, um Frames mithilfe von maschinellem Lernen zu verarbeiten und Filter anzuwenden. Das verarbeitete eingehende Video kann dann auf der Empfängerseite gerendert werden.

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

Wichtig

Dieses Feature von Azure Communication Services befindet sich derzeit in der Vorschau.

Vorschau-APIs und -SDKs werden ohne Vereinbarung zum Servicelevel bereitgestellt. Es wird empfohlen, diese nicht für Produktionsworkloads zu verwenden. Einige Features werden möglicherweise nicht unterstützt oder bieten nur eingeschränkte Funktionalität.

Weitere Informationen finden Sie in den ergänzenden Nutzungsbestimmungen für Microsoft Azure-Vorschauversionen.

Der Zugriff auf die Rohbildschirmfreigabe erfolgt über die öffentliche Vorschau und ist im Rahmen von Version 1.15.1-beta.1+ verfügbar.

Zugriff auf die Rohbildschirmfreigabe

Rohbildschirm-Freigabemedien gewähren speziell Zugriff auf das MediaStream-Objekt für eingehende und ausgehende Bildschirmfreigabestreams. Bei der Rohbildschirmfreigabe können Sie dieses Objekt verwenden, um mithilfe von maschinellem Lernen Filter anzuwenden und die Bildschirmfreigabe-Frames zu verarbeiten.

Verarbeitete ausgehende Rohbildschirm-Frames können als ausgehende Bildschirmfreigabe des Senders gesendet werden. Verarbeitete eingehende Rohbildschirm-Frames können auf der Empfängerseite gerendert werden.

Hinweis: Das Senden von Bildschirmfreigaben wird nur im Desktopbrowser unterstützt.

Startbildschirmfreigabe mit einem benutzerdefinierten Bildschirmfreigabestream

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'

Zugreifen auf den Rohbildschirm-Freigabestream über einen Bildschirm, eine Browserregisterkarte oder eine App, und Anwenden von Effekten auf den Stream

Im Folgenden finden Sie ein Beispiel zum Anwenden eines Schwarzweißeffekts auf den rohen Stream der Bildschirmfreigabe über einen Bildschirm, eine Browserregisterkarte oder eine App. HINWEIS: Die Canvas-API „context filter = "grayscale(1)"“ wird in Safari nicht unterstützt.

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;

Beenden des Sendens des Bildschirmfreigabestreams

Verwenden Sie den folgenden Code, um das Senden eines benutzerdefinierten Bildschirmstreams zu beenden, nachdem er während eines Anrufs festgelegt wurde.

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

Zugreifen auf den eingehenden Bildschirmfreigabestream von einem Remoteteilnehmer

Sie können von einem Remoteteilnehmer aus auf den Rohbildschirm-Freigabestream zugreifen. Sie verwenden MediaStream für den eingehenden Rohbildschirm-Freigabestream, um Frames mithilfe von maschinellem Lernen zu verarbeiten und Filter anzuwenden. Der verarbeitete eingehende Bildschirmfreigabestream kann dann auf der Empfängerseite gerendert werden.

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

Nächste Schritte

Weitere Informationen finden Sie in den folgenden Artikeln: