Udostępnij za pośrednictwem


Szybki start: dodawanie dostępu do nieprzetworzonych multimediów do aplikacji

Z tego przewodnika Szybki start dowiesz się, jak zaimplementować dostęp do nieprzetworzonych multimediów przy użyciu zestawu SDK wywołującego usługi Azure Communication Services dla aparatu Unity. Zestaw SDK wywołujący usługi Azure Communication Services oferuje interfejsy API, które umożliwiają aplikacjom generowanie własnych ramek wideo w celu wysyłania lub renderowania nieprzetworzonych ramek wideo od zdalnych uczestników wywołania. Ten przewodnik Szybki start jest oparty na przewodniku Szybki start: dodawanie wywołania wideo 1:1 do aplikacji dla aparatu Unity.

Dostęp do rawvideo

Ponieważ aplikacja generuje ramki wideo, aplikacja musi poinformować usługę Azure Communication Services Calling SDK o formatach wideo, które aplikacja może wygenerować. Te informacje pozwalają zestawowi SDK wywołującym usługi Azure Communication Services wybrać najlepszą konfigurację formatu wideo dla warunków sieciowych w tym czasie.

Wirtualne wideo

Obsługiwane rozwiązania wideo

Współczynnik proporcji Rozwiązanie Maksymalna liczba klatek na sekundę
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 SV (640x480) 30
4x3 424x320 15
4x3 QVGA (320x240) 15
4x3 212x160 15
  1. Wykonaj kroki opisane w tym przewodniku Szybki start: dodawanie wywołania wideo 1:1 do aplikacji w celu utworzenia gry aparatu Unity. Celem jest uzyskanie obiektu gotowego CallAgent do rozpoczęcia wywołania. Znajdź sfinalizowany kod dla tego przewodnika Szybki start w witrynie GitHub.

  2. Utwórz tablicę VideoFormat przy użyciu formatu VideoStreamPixelFormat obsługiwanego przez zestaw SDK. Gdy dostępnych jest wiele formatów, kolejność formatów na liście nie ma wpływu na ten format ani nie określa priorytetów używanych formatów. Kryteria wyboru formatu są oparte na czynnikach zewnętrznych, takich jak przepustowość sieci.

    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. Utwórz RawOutgoingVideoStreamOptionselement i ustaw przy Formats użyciu wcześniej utworzonego obiektu.

    var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions
    {
        Formats = videoStreamFormats
    };
    
  4. Utwórz wystąpienie klasy przy użyciu utworzonego RawOutgoingVideoStreamOptions wcześniej wystąpieniaVirtualOutgoingVideoStream.

    var rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  5. Subskrybuj pełnomocnika RawOutgoingVideoStream.FormatChanged . To zdarzenie informuje o każdej VideoStreamFormat zmianie z jednego z formatów wideo podanych na liście.

    rawOutgoingVideoStream.FormatChanged += (object sender, VideoStreamFormatChangedEventArgs args)
    {
        VideoStreamFormat videoStreamFormat = args.Format;
    }
    
  6. Subskrybuj pełnomocnika RawOutgoingVideoStream.StateChanged . To zdarzenie informuje o każdej State zmianie.

    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. Obsługa nieprzetworzonych wychodzących transakcji stanu strumienia wideo, takich jak Uruchamianie i zatrzymywanie, i rozpoczynanie generowania niestandardowych ramek wideo lub zawieszenie algorytmu generowania ramki.

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

    Oto przykład wychodzącego generatora klatek wideo:

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

    Uwaga

    unsafe Modyfikator jest używany w tej metodzie, ponieważ NativeBuffer wymaga dostępu do zasobów pamięci natywnej. W związku z Allow unsafe tym należy również włączyć opcję w edytorze aparatu Unity.

  8. Podobnie możemy obsługiwać przychodzące klatki wideo w odpowiedzi na zdarzenie strumienia StateChanged wideo.

    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. Zdecydowanie zaleca się zarządzanie zarówno przychodzącymi, jak i wychodzącymi ramkami wideo za pomocą mechanizmu buforowania, aby uniknąć przeciążenia MonoBehaviour.Update() metody wywołania zwrotnego, które powinny być lekkie i unikać dużych obowiązków procesora CPU lub sieci i zapewnić sprawniejsze środowisko wideo. Ta opcjonalna optymalizacja jest pozostawiona deweloperom, aby zdecydować, co działa najlepiej w swoich scenariuszach.

    Oto przykład sposobu renderowania ramek przychodzących do aparatu Unity VideoTexture przez wywołanie Graphics.Blit kolejki wewnętrznej:

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

Z tego przewodnika Szybki start dowiesz się, jak zaimplementować dostęp do nieprzetworzonych multimediów przy użyciu zestawu SDK wywołującego usługi Azure Communication Services dla systemu Windows. Zestaw SDK wywołujący usługi Azure Communication Services oferuje interfejsy API, które umożliwiają aplikacjom generowanie własnych ramek wideo do wysyłania do zdalnych uczestników połączenia. Ten przewodnik Szybki start jest oparty na przewodniku Szybki start: dodawanie wywołania wideo 1:1 do aplikacji dla systemu Windows.

Dostęp rawAudio

Uzyskiwanie dostępu do nieprzetworzonych multimediów audio zapewnia dostęp do strumienia audio połączenia przychodzącego oraz możliwość wyświetlania i wysyłania niestandardowych wychodzących strumieni audio podczas połączenia.

Wysyłanie nieprzetworzonego dźwięku wychodzącego

Ustaw obiekt opcji określający właściwości nieprzetworzonego strumienia, które chcemy wysłać.

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

Utwórz element RawOutgoingAudioStream i dołącz go, aby dołączyć opcje wywołania, a strumień jest uruchamiany automatycznie po nawiązaniu połączenia.

    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.

Dołączanie strumienia do wywołania

Możesz też dołączyć strumień do istniejącego Call wystąpienia:

    await call.StartAudio(rawOutgoingAudioStream);

Rozpoczynanie wysyłania nieprzetworzonych przykładów

Możemy zacząć wysyłać dane tylko wtedy, gdy stan strumienia to AudioStreamState.Started. Aby zaobserwować zmianę stanu strumienia audio, dodaj odbiornik do OnStateChangedListener zdarzenia.

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

Po uruchomieniu strumienia możemy rozpocząć wysyłanie MemoryBuffer przykładów dźwiękowych do wywołania. Format buforu audio powinien być zgodny z określonymi właściwościami strumienia.

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

Odbieranie nieprzetworzonego przychodzącego dźwięku

Możemy również odbierać próbki strumienia audio połączenia, tak jakbyśmy MemoryBuffer chcieli przetworzyć strumień audio połączenia przed odtwarzaniem. RawIncomingAudioStreamOptions Utwórz obiekt określający właściwości nieprzetworzonego strumienia, które chcemy otrzymać.

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

Tworzenie elementu RawIncomingAudioStream i dołączanie go w celu dołączenia do opcji wywołania

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

Można również dołączyć strumień do istniejącego Call wystąpienia:

    await call.startAudio(context, rawIncomingAudioStream);

Aby rozpocząć odbieranie nieprzetworzonych audio ze strumienia przychodzącego, dodaj odbiorniki do stanu strumienia przychodzącego i bufor odebrane zdarzenia.

    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;

Dostęp do rawvideo

Ponieważ aplikacja generuje ramki wideo, aplikacja musi poinformować usługę Azure Communication Services Calling SDK o formatach wideo, które aplikacja może wygenerować. Te informacje pozwalają zestawowi SDK wywołującym usługi Azure Communication Services wybrać najlepszą konfigurację formatu wideo dla warunków sieciowych w tym czasie.

Wirtualne wideo

Obsługiwane rozwiązania wideo

Współczynnik proporcji Rozwiązanie Maksymalna liczba klatek na sekundę
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 SV (640x480) 30
4x3 424x320 15
4x3 QVGA (320x240) 15
4x3 212x160 15
  1. Utwórz tablicę VideoFormat przy użyciu formatu VideoStreamPixelFormat obsługiwanego przez zestaw SDK. Gdy dostępnych jest wiele formatów, kolejność formatów na liście nie ma wpływu na ten format ani nie określa priorytetów używanych formatów. Kryteria wyboru formatu są oparte na czynnikach zewnętrznych, takich jak przepustowość sieci.

    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. Utwórz RawOutgoingVideoStreamOptionselement i ustaw przy Formats użyciu wcześniej utworzonego obiektu.

    var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions
    {
        Formats = videoStreamFormats
    };
    
  3. Utwórz wystąpienie klasy przy użyciu utworzonego RawOutgoingVideoStreamOptions wcześniej wystąpieniaVirtualOutgoingVideoStream.

    var rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  4. Subskrybuj pełnomocnika RawOutgoingVideoStream.FormatChanged . To zdarzenie informuje o każdej VideoStreamFormat zmianie z jednego z formatów wideo podanych na liście.

    rawOutgoingVideoStream.FormatChanged += (object sender, VideoStreamFormatChangedEventArgs args)
    {
        VideoStreamFormat videoStreamFormat = args.Format;
    }
    
  5. Utwórz wystąpienie następującej klasy pomocniczej, aby uzyskać dostęp do danych buforu

    [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. Utwórz wystąpienie następującej klasy pomocniczej, aby wygenerować losowe RawVideoFramewartości przy użyciu polecenia VideoStreamPixelFormat.Rgba

    public class VideoFrameSender
    {
        private RawOutgoingVideoStream rawOutgoingVideoStream;
        private RawVideoFrameKind rawVideoFrameKind;
        private Thread frameIteratorThread;
        private Random random = new Random();
        private volatile bool stopFrameIterator = false;
        public VideoFrameSender(RawVideoFrameKind rawVideoFrameKind, RawOutgoingVideoStream rawOutgoingVideoStream)
        {
            this.rawVideoFrameKind = rawVideoFrameKind;
            this.rawOutgoingVideoStream = rawOutgoingVideoStream;
        }
        public async void VideoFrameIterator()
        {
            while (!stopFrameIterator)
            {
                if (rawOutgoingVideoStream != null &&
                    rawOutgoingVideoStream.Format != null &&
                    rawOutgoingVideoStream.State == VideoStreamState.Started)
                {
                    await SendRandomVideoFrameRGBA();
                }
            }
        }
        private async Task SendRandomVideoFrameRGBA()
        {
            uint rgbaCapacity = (uint)(rawOutgoingVideoStream.Format.Width * rawOutgoingVideoStream.Format.Height * 4);
            RawVideoFrame videoFrame = null;
            switch (rawVideoFrameKind)
            {
                case RawVideoFrameKind.Buffer:
                    videoFrame = 
                        GenerateRandomVideoFrameBuffer(rawOutgoingVideoStream.Format, rgbaCapacity);
                    break;
                case RawVideoFrameKind.Texture:
                    videoFrame = 
                        GenerateRandomVideoFrameTexture(rawOutgoingVideoStream.Format, rgbaCapacity);
                    break;
            }
            try
            {
                using (videoFrame)
                {
                    await rawOutgoingVideoStream.SendRawVideoFrameAsync(videoFrame);
                }
            }
            catch (Exception ex)
            {
                string msg = ex.Message;
            }
            try
            {
                int delayBetweenFrames = (int)(1000.0 / rawOutgoingVideoStream.Format.FramesPerSecond);
                await Task.Delay(delayBetweenFrames);
            }
            catch (Exception ex)
            {
                string msg = ex.Message;
            }
        }
        private unsafe RawVideoFrame GenerateRandomVideoFrameBuffer(VideoStreamFormat videoFormat, uint rgbaCapacity)
        {
            var rgbaBuffer = new MemoryBuffer(rgbaCapacity);
            byte* rgbaArrayBuffer = BufferExtensions.GetArrayBuffer(rgbaBuffer);
            GenerateRandomVideoFrame(&rgbaArrayBuffer);
            return new RawVideoFrameBuffer()
            {
                Buffers = new MemoryBuffer[] { rgbaBuffer },
                StreamFormat = videoFormat
            };
        }
        private unsafe RawVideoFrame GenerateRandomVideoFrameTexture(VideoStreamFormat videoFormat, uint rgbaCapacity)
        {
            var timeSpan = new TimeSpan(rawOutgoingVideoStream.TimestampInTicks);
            var rgbaBuffer = new Buffer(rgbaCapacity)
            {
                Length = rgbaCapacity
            };
            byte* rgbaArrayBuffer = BufferExtensions.GetArrayBuffer(rgbaBuffer);
            GenerateRandomVideoFrame(&rgbaArrayBuffer);
            var mediaStreamSample = MediaStreamSample.CreateFromBuffer(rgbaBuffer, timeSpan);
            return new RawVideoFrameTexture()
            {
                Texture = mediaStreamSample,
                StreamFormat = videoFormat
            };
        }
        private unsafe void GenerateRandomVideoFrame(byte** rgbaArrayBuffer)
        {
            int w = rawOutgoingVideoStream.Format.Width;
            int h = rawOutgoingVideoStream.Format.Height;
            byte r = (byte)random.Next(1, 255);
            byte g = (byte)random.Next(1, 255);
            byte b = (byte)random.Next(1, 255);
            int rgbaStride = w * 4;
            for (int y = 0; y < h; y++)
            {
                for (int x = 0; x < rgbaStride; x += 4)
                {
                    (*rgbaArrayBuffer)[(w * 4 * y) + x + 0] = (byte)(y % r);
                    (*rgbaArrayBuffer)[(w * 4 * y) + x + 1] = (byte)(y % g);
                    (*rgbaArrayBuffer)[(w * 4 * y) + x + 2] = (byte)(y % b);
                    (*rgbaArrayBuffer)[(w * 4 * y) + x + 3] = 255;
                }
            }
        }
        public void Start()
        {
            frameIteratorThread = new Thread(VideoFrameIterator);
            frameIteratorThread.Start();
        }
        public void Stop()
        {
            try
            {
                if (frameIteratorThread != null)
                {
                    stopFrameIterator = true;
                    frameIteratorThread.Join();
                    frameIteratorThread = null;
                    stopFrameIterator = false;
                }
            }
            catch (Exception ex)
            {
                string msg = ex.Message;
            }
        }
    }
    
  7. Subskrybuj pełnomocnika VideoStream.StateChanged . To zdarzenie informuje o stanie bieżącego strumienia. Nie wysyłaj ramek, jeśli stan nie jest równy VideoStreamState.Started.

    private VideoFrameSender videoFrameSender;
    rawOutgoingVideoStream.StateChanged += (object sender, VideoStreamStateChangedEventArgs args) =>
    {
        CallVideoStream callVideoStream = args.Stream;
        switch (callVideoStream.State)
            {
                case VideoStreamState.Available:
                    // VideoStream has been attached to the call
                    var frameKind = RawVideoFrameKind.Buffer; // Use the frameKind you prefer
                    //var frameKind = RawVideoFrameKind.Texture;
                    videoFrameSender = new VideoFrameSender(frameKind, rawOutgoingVideoStream);
                    break;
                case VideoStreamState.Started:
                    // Start sending frames
                    videoFrameSender.Start();
                    break;
                case VideoStreamState.Stopped:
                    // Stop sending frames
                    videoFrameSender.Stop();
                    break;
            }
    };
    

Udostępnianie ekranu wideo

Ponieważ system Windows generuje ramki, musisz zaimplementować własną usługę pierwszego planu, aby przechwycić ramki i wysłać je przy użyciu interfejsu API wywołującego usługi Azure Communication Services.

Obsługiwane rozwiązania wideo

Współczynnik proporcji Rozwiązanie Maksymalna liczba klatek na sekundę
Cokolwiek Wszystko do 1080p 30

Kroki tworzenia strumienia wideo udziału ekranu

  1. Utwórz tablicę VideoFormat przy użyciu formatu VideoStreamPixelFormat obsługiwanego przez zestaw SDK. Gdy dostępnych jest wiele formatów, kolejność formatów na liście nie ma wpływu na ten format ani nie określa priorytetów używanych formatów. Kryteria wyboru formatu są oparte na czynnikach zewnętrznych, takich jak przepustowość sieci.
    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. Utwórz RawOutgoingVideoStreamOptionselement i ustaw przy VideoFormats użyciu wcześniej utworzonego obiektu.
    var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions
    {
        Formats = videoStreamFormats
    };
    
  3. Utwórz wystąpienie klasy przy użyciu utworzonego RawOutgoingVideoStreamOptions wcześniej wystąpieniaVirtualOutgoingVideoStream.
    var rawOutgoingVideoStream = new ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  4. Przechwyć i wysłać ramkę wideo w następujący sposób.
    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;
        }
    }
    

Nieprzetworzone przychodzące wideo

Ta funkcja zapewnia dostęp do ramek wideo wewnątrz IncomingVideoStreamobiektu w celu manipulowania tymi strumieniami lokalnie

  1. Tworzenie wystąpienia tego IncomingVideoOptions zestawu za pomocą JoinCallOptions ustawienia VideoStreamKind.RawIncoming
    var frameKind = RawVideoFrameKind.Buffer;  // Use the frameKind you prefer to receive
    var incomingVideoOptions = new IncomingVideoOptions
    {
        StreamKind = VideoStreamKind.RawIncoming,
        FrameKind = frameKind
    };
    var joinCallOptions = new JoinCallOptions
    {
        IncomingVideoOptions = incomingVideoOptions
    };
    
  2. Po otrzymaniu delegata dołączania RemoteParticipant.VideoStreamStateChanged zdarzeńParticipantsUpdatedEventArgs. To zdarzenie informuje o stanie IncomingVideoStream obiektów.
    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. W tym czasie element IncomingVideoStream ma VideoStreamState.Available delegata dołączania RawIncomingVideoStream.RawVideoFrameReceived stanu, jak pokazano w poprzednim kroku. Zapewnia to nowe RawVideoFrame obiekty.
    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;
        }
    }
    

Z tego przewodnika Szybki start dowiesz się, jak zaimplementować dostęp do nieprzetworzonych multimediów przy użyciu zestawu SDK wywołującego usługi Azure Communication Services dla systemu Android.

Zestaw SDK wywołujący usługi Azure Communication Services oferuje interfejsy API, które umożliwiają aplikacjom generowanie własnych ramek wideo do wysyłania do zdalnych uczestników połączenia.

Ten przewodnik Szybki start jest oparty na przewodniku Szybki start: dodawanie wywołania wideo 1:1 do aplikacji dla systemu Android.

Dostęp rawAudio

Uzyskiwanie dostępu do nieprzetworzonych multimediów audio zapewnia dostęp do przychodzącego strumienia audio połączenia oraz możliwość wyświetlania i wysyłania niestandardowych wychodzących strumieni audio podczas połączenia.

Wysyłanie nieprzetworzonego dźwięku wychodzącego

Ustaw obiekt opcji określający właściwości nieprzetworzonego strumienia, które chcemy wysłać.

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

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

Utwórz element RawOutgoingAudioStream i dołącz go, aby dołączyć opcje wywołania, a strumień jest uruchamiany automatycznie po nawiązaniu połączenia.

    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.

Dołączanie strumienia do wywołania

Możesz też dołączyć strumień do istniejącego Call wystąpienia:

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

Rozpoczynanie wysyłania nieprzetworzonych przykładów

Możemy zacząć wysyłać dane tylko wtedy, gdy stan strumienia to AudioStreamState.STARTED. Aby zaobserwować zmianę stanu strumienia audio, dodaj odbiornik do OnStateChangedListener zdarzenia.

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

    rawOutgoingAudioStream.addOnStateChangedListener(this::onStateChanged)

Po uruchomieniu strumienia możemy rozpocząć wysyłanie java.nio.ByteBuffer przykładów dźwiękowych do wywołania.

Format buforu audio powinien być zgodny z określonymi właściwościami strumienia.

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

Odbieranie nieprzetworzonego przychodzącego dźwięku

Możemy również odbierać próbki strumienia audio połączenia, tak jakbyśmy java.nio.ByteBuffer chcieli przetworzyć dźwięk przed odtworzeniem.

RawIncomingAudioStreamOptions Utwórz obiekt określający właściwości nieprzetworzonego strumienia, które chcemy otrzymać.

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

Tworzenie elementu RawIncomingAudioStream i dołączanie go w celu dołączenia do opcji wywołania

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

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

Można również dołączyć strumień do istniejącego Call wystąpienia:


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

Aby rozpocząć odbieranie nieprzetworzonych audio ze strumienia przychodzącego, dodaj odbiorniki do stanu strumienia przychodzącego i bufor odebrane zdarzenia.

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

Ważne jest również, aby pamiętać o zatrzymaniu strumienia audio w bieżącym wystąpieniu połączenia Call :


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

Dostęp do rawvideo

Ponieważ aplikacja generuje ramki wideo, aplikacja musi poinformować usługę Azure Communication Services Calling SDK o formatach wideo, które aplikacja może wygenerować. Te informacje pozwalają zestawowi SDK wywołującym usługi Azure Communication Services wybrać najlepszą konfigurację formatu wideo dla warunków sieciowych w tym czasie.

Wirtualne wideo

Obsługiwane rozwiązania wideo

Współczynnik proporcji Rozwiązanie Maksymalna liczba klatek na sekundę
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 SV (640x480) 30
4x3 424x320 15
4x3 QVGA (320x240) 15
4x3 212x160 15
  1. Utwórz tablicę VideoFormat przy użyciu formatu VideoStreamPixelFormat obsługiwanego przez zestaw SDK.

    Gdy dostępnych jest wiele formatów, kolejność formatów na liście nie ma wpływu na ten format ani nie określa priorytetów używanych formatów. Kryteria wyboru formatu są oparte na czynnikach zewnętrznych, takich jak przepustowość sieci.

    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. Utwórz RawOutgoingVideoStreamOptionselement i ustaw przy Formats użyciu wcześniej utworzonego obiektu.

    RawOutgoingVideoStreamOptions rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions();
    rawOutgoingVideoStreamOptions.setFormats(videoStreamFormats);
    
  3. Utwórz wystąpienie klasy przy użyciu utworzonego RawOutgoingVideoStreamOptions wcześniej wystąpieniaVirtualOutgoingVideoStream.

    VirtualOutgoingVideoStream rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  4. Subskrybuj pełnomocnika RawOutgoingVideoStream.addOnFormatChangedListener . To zdarzenie informuje o każdej VideoStreamFormat zmianie z jednego z formatów wideo podanych na liście.

    virtualOutgoingVideoStream.addOnFormatChangedListener((VideoStreamFormatChangedEvent args) -> 
    {
        VideoStreamFormat videoStreamFormat = args.Format;
    });
    
  5. Utwórz wystąpienie następującej klasy pomocniczej, aby wygenerować losowe RawVideoFramewartości przy użyciu polecenia VideoStreamPixelFormat.RGBA

    public class VideoFrameSender
    {
        private RawOutgoingVideoStream rawOutgoingVideoStream;
        private Thread frameIteratorThread;
        private Random random = new Random();
        private volatile boolean stopFrameIterator = false;
    
        public VideoFrameSender(RawOutgoingVideoStream rawOutgoingVideoStream)
        {
            this.rawOutgoingVideoStream = rawOutgoingVideoStream;
        }
    
        public void VideoFrameIterator()
        {
            while (!stopFrameIterator)
            {
                if (rawOutgoingVideoStream != null && 
                    rawOutgoingVideoStream.getFormat() != null && 
                    rawOutgoingVideoStream.getState() == VideoStreamState.STARTED)
                {
                    SendRandomVideoFrameRGBA();
                }
            }
        }
    
        private void SendRandomVideoFrameRGBA()
        {
            int rgbaCapacity = rawOutgoingVideoStream.getFormat().getWidth() * rawOutgoingVideoStream.getFormat().getHeight() * 4;
    
            RawVideoFrame videoFrame = GenerateRandomVideoFrameBuffer(rawOutgoingVideoStream.getFormat(), rgbaCapacity);
    
            try
            {
                rawOutgoingVideoStream.sendRawVideoFrame(videoFrame).get();
    
                int delayBetweenFrames = (int)(1000.0 / rawOutgoingVideoStream.getFormat().getFramesPerSecond());
                Thread.sleep(delayBetweenFrames);
            }
            catch (Exception ex)
            {
                String msg = ex.getMessage();
            }
            finally
            {
                videoFrame.close();
            }
        }
    
        private RawVideoFrame GenerateRandomVideoFrameBuffer(VideoStreamFormat videoStreamFormat, int rgbaCapacity)
        {
            ByteBuffer rgbaBuffer = ByteBuffer.allocateDirect(rgbaCapacity); // Only allocateDirect ByteBuffers are allowed
            rgbaBuffer.order(ByteOrder.nativeOrder());
    
            GenerateRandomVideoFrame(rgbaBuffer, rgbaCapacity);
    
            RawVideoFrameBuffer videoFrameBuffer = new RawVideoFrameBuffer();
            videoFrameBuffer.setBuffers(Arrays.asList(rgbaBuffer));
            videoFrameBuffer.setStreamFormat(videoStreamFormat);
    
            return videoFrameBuffer;
        }
    
        private void GenerateRandomVideoFrame(ByteBuffer rgbaBuffer, int rgbaCapacity)
        {
            int w = rawOutgoingVideoStream.getFormat().getWidth();
            int h = rawOutgoingVideoStream.getFormat().getHeight();
    
            byte rVal = (byte)random.nextInt(255);
            byte gVal = (byte)random.nextInt(255);
            byte bVal = (byte)random.nextInt(255);
            byte aVal = (byte)255;
    
            byte[] rgbaArrayBuffer = new byte[rgbaCapacity];
    
            int rgbaStride = w * 4;
    
            for (int y = 0; y < h; y++)
            {
                for (int x = 0; x < rgbaStride; x += 4)
                {
                    rgbaArrayBuffer[(w * 4 * y) + x + 0] = rVal;
                    rgbaArrayBuffer[(w * 4 * y) + x + 1] = gVal;
                    rgbaArrayBuffer[(w * 4 * y) + x + 2] = bVal;
                    rgbaArrayBuffer[(w * 4 * y) + x + 3] = aVal;
                }
            }
    
            rgbaBuffer.put(rgbaArrayBuffer);
            rgbaBuffer.rewind();
        }
    
        public void Start()
        {
            frameIteratorThread = new Thread(this::VideoFrameIterator);
            frameIteratorThread.start();
        }
    
        public void Stop()
        {
            try
            {
                if (frameIteratorThread != null)
                {
                    stopFrameIterator = true;
    
                    frameIteratorThread.join();
                    frameIteratorThread = null;
    
                    stopFrameIterator = false;
                }
            }
            catch (InterruptedException ex)
            {
                String msg = ex.getMessage();
            }
        }
    }
    
  6. Subskrybuj pełnomocnika VideoStream.addOnStateChangedListener . Ten delegat informuje o stanie bieżącego strumienia. Nie wysyłaj ramek, jeśli stan nie jest równy VideoStreamState.STARTED.

    private VideoFrameSender videoFrameSender;
    
    rawOutgoingVideoStream.addOnStateChangedListener((VideoStreamStateChangedEvent args) ->
    {
        CallVideoStream callVideoStream = args.getStream();
    
        switch (callVideoStream.getState())
        {
            case AVAILABLE:
                videoFrameSender = new VideoFrameSender(rawOutgoingVideoStream);
                break;
            case STARTED:
                // Start sending frames
                videoFrameSender.Start();
                break;
            case STOPPED:
                // Stop sending frames
                videoFrameSender.Stop();
                break;
        }
    });
    

ScreenShare Video

Ponieważ system Windows generuje ramki, musisz zaimplementować własną usługę pierwszego planu, aby przechwycić ramki i wysłać je przy użyciu interfejsu API wywołującego usługi Azure Communication Services.

Obsługiwane rozwiązania wideo

Współczynnik proporcji Rozwiązanie Maksymalna liczba klatek na sekundę
Cokolwiek Wszystko do 1080p 30

Kroki tworzenia strumienia wideo udziału ekranu

  1. Utwórz tablicę VideoFormat przy użyciu formatu VideoStreamPixelFormat obsługiwanego przez zestaw SDK.

    Gdy dostępnych jest wiele formatów, kolejność formatów na liście nie ma wpływu na ten format ani nie określa priorytetów używanych formatów. Kryteria wyboru formatu są oparte na czynnikach zewnętrznych, takich jak przepustowość sieci.

    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. Utwórz RawOutgoingVideoStreamOptionselement i ustaw przy VideoFormats użyciu wcześniej utworzonego obiektu.

    RawOutgoingVideoStreamOptions rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions();
    rawOutgoingVideoStreamOptions.setFormats(videoStreamFormats);
    
  3. Utwórz wystąpienie klasy przy użyciu utworzonego RawOutgoingVideoStreamOptions wcześniej wystąpieniaVirtualOutgoingVideoStream.

    ScreenShareOutgoingVideoStream rawOutgoingVideoStream = new ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  4. Przechwyć i wysłać ramkę wideo w następujący sposób.

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

Nieprzetworzone przychodzące wideo

Ta funkcja zapewnia dostęp do ramek wideo wewnątrz IncomingVideoStream obiektów w celu manipulowania tymi ramkami lokalnie

  1. Tworzenie wystąpienia tego IncomingVideoOptions zestawu za pomocą JoinCallOptions ustawienia VideoStreamKind.RawIncoming

    IncomingVideoOptions incomingVideoOptions = new IncomingVideoOptions()
            .setStreamType(VideoStreamKind.RAW_INCOMING);
    
    JoinCallOptions joinCallOptions = new JoinCallOptions()
            .setIncomingVideoOptions(incomingVideoOptions);
    
  2. Po otrzymaniu delegata dołączania RemoteParticipant.VideoStreamStateChanged zdarzeńParticipantsUpdatedEventArgs. To zdarzenie informuje o stanie IncomingVideoStream obiektu.

    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. W tym czasie element IncomingVideoStream ma VideoStreamState.Available delegata dołączania RawIncomingVideoStream.RawVideoFrameReceived stanu, jak pokazano w poprzednim kroku. Ten delegat udostępnia nowe RawVideoFrame obiekty.

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

Z tego przewodnika Szybki start dowiesz się, jak zaimplementować dostęp do nieprzetworzonych multimediów przy użyciu zestawu SDK wywołującego usługi Azure Communication Services dla systemu iOS.

Zestaw SDK wywołujący usługi Azure Communication Services oferuje interfejsy API, które umożliwiają aplikacjom generowanie własnych ramek wideo do wysyłania do zdalnych uczestników połączenia.

Ten przewodnik Szybki start jest oparty na przewodniku Szybki start: dodawanie wideo 1:1 do aplikacji dla systemu iOS.

Dostęp rawAudio

Uzyskiwanie dostępu do nieprzetworzonych multimediów audio zapewnia dostęp do strumienia audio połączenia przychodzącego oraz możliwość wyświetlania i wysyłania niestandardowych wychodzących strumieni audio podczas połączenia.

Wysyłanie nieprzetworzonego dźwięku wychodzącego

Ustaw obiekt opcji określający właściwości nieprzetworzonego strumienia, które chcemy wysłać.

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

Utwórz element RawOutgoingAudioStream i dołącz go, aby dołączyć opcje wywołania, a strumień jest uruchamiany automatycznie po nawiązaniu połączenia.

    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.

Dołączanie strumienia do wywołania

Możesz też dołączyć strumień do istniejącego Call wystąpienia:


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

Rozpoczynanie wysyłania nieprzetworzonych przykładów

Możemy zacząć wysyłać dane tylko wtedy, gdy stan strumienia to AudioStreamState.started. Aby zaobserwować zmianę stanu strumienia audio, zaimplementujemy element RawOutgoingAudioStreamDelegate. Ustaw go jako delegata strumienia.

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

    self.rawOutgoingAudioStream.delegate = DelegateImplementer()

lub użyj zamknięcia na podstawie

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

Po uruchomieniu strumienia możemy rozpocząć wysyłanie AVAudioPCMBuffer przykładów dźwiękowych do wywołania.

Format buforu audio powinien być zgodny z określonymi właściwościami strumienia.

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

Ważne jest również, aby pamiętać o zatrzymaniu strumienia audio w bieżącym wystąpieniu połączenia Call :


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

Przechwytywanie przykładów mikrofonu

Za pomocą firmy Apple AVAudioEngine możemy przechwytywać ramki mikrofonu, naciskając do węzła wejściowego aparatu audio. Przechwytywanie danych mikrofonu i możliwość korzystania z nieprzetworzonych funkcji audio umożliwia przetwarzanie dźwięku przed wysłaniem go do wywołania.

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

Uwaga

Częstotliwość próbkowania węzła wejściowego aparatu audio jest domyślnie ustawiona na >wartość preferowanej częstotliwości próbkowania dla udostępnionej sesji audio. Nie można więc zainstalować naciśnięcia w tym węźle przy użyciu innej wartości. Dlatego musimy upewnić się, że RawOutgoingStream częstotliwość próbkowania właściwości jest zgodna z tym, który uzyskujemy z poziomu próbki mikrofonu lub konwertujemy naciśnięcia na format zgodny z oczekiwanymi wartościami w strumieniu wychodzącym.

W tym małym przykładzie dowiedzieliśmy się, jak możemy przechwycić dane mikrofonu AVAudioEngine i wysłać te przykłady do wywołania przy użyciu nieprzetworzonej funkcji audio wychodzącej.

Odbieranie nieprzetworzonego przychodzącego dźwięku

Możemy również odbierać próbki strumienia audio połączenia, tak jakbyśmy AVAudioPCMBuffer chcieli przetworzyć dźwięk przed odtworzeniem.

RawIncomingAudioStreamOptions Utwórz obiekt określający właściwości nieprzetworzonego strumienia, które chcemy otrzymać.

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

Tworzenie elementu RawOutgoingAudioStream i dołączanie go w celu dołączenia do opcji wywołania

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

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

Można również dołączyć strumień do istniejącego Call wystąpienia:


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

Aby rozpocząć odbieranie nieprzetworzonego buforu audio ze strumienia przychodzącego, zaimplementuj element 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()

lub

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

Dostęp do rawvideo

Ponieważ aplikacja generuje ramki wideo, aplikacja musi poinformować usługę Azure Communication Services Calling SDK o formatach wideo, które aplikacja może wygenerować. Te informacje pozwalają zestawowi SDK wywołującym usługi Azure Communication Services wybrać najlepszą konfigurację formatu wideo dla warunków sieciowych w tym czasie.

Wirtualne wideo

Obsługiwane rozwiązania wideo

Współczynnik proporcji Rozwiązanie Maksymalna liczba klatek na sekundę
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 SV (640x480) 30
4x3 424x320 15
4x3 QVGA (320x240) 15
4x3 212x160 15
  1. Utwórz tablicę VideoFormat przy użyciu formatu VideoStreamPixelFormat obsługiwanego przez zestaw SDK. Gdy dostępnych jest wiele formatów, kolejność formatów na liście nie ma wpływu na ten format ani nie określa priorytetów używanych formatów. Kryteria wyboru formatu są oparte na czynnikach zewnętrznych, takich jak przepustowość sieci.

    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. Utwórz RawOutgoingVideoStreamOptionsformaty , i ustaw za pomocą wcześniej utworzonego obiektu.

    var rawOutgoingVideoStreamOptions = RawOutgoingVideoStreamOptions()
    rawOutgoingVideoStreamOptions.formats = videoStreamFormats
    
  3. Utwórz wystąpienie klasy przy użyciu utworzonego RawOutgoingVideoStreamOptions wcześniej wystąpieniaVirtualOutgoingVideoStream.

    var rawOutgoingVideoStream = VirtualOutgoingVideoStream(videoStreamOptions: rawOutgoingVideoStreamOptions)
    
  4. Zaimplementuj do delegata VirtualOutgoingVideoStreamDelegate . Zdarzenie didChangeFormat informuje o każdej VideoStreamFormat zmianie z jednego z formatów wideo podanych na liście.

    virtualOutgoingVideoStream.delegate = /* Attach delegate and implement didChangeFormat */
    
  5. Tworzenie wystąpienia następującej klasy pomocniczej w celu uzyskania dostępu do CVPixelBuffer danych

    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. Utwórz wystąpienie następującej klasy pomocniczej, aby wygenerować losowe RawVideoFrameBufferwartości przy użyciu polecenia VideoStreamPixelFormat.rgba

    final class VideoFrameSender : NSObject
    {
        private var rawOutgoingVideoStream: RawOutgoingVideoStream
        private var frameIteratorThread: Thread
        private var stopFrameIterator: Bool = false
    
        public VideoFrameSender(rawOutgoingVideoStream: RawOutgoingVideoStream)
        {
            self.rawOutgoingVideoStream = rawOutgoingVideoStream
        }
    
        @objc private func VideoFrameIterator()
        {
            while !stopFrameIterator {
                if rawOutgoingVideoStream != nil &&
                   rawOutgoingVideoStream.format != nil &&
                   rawOutgoingVideoStream.state == .started {
                    SendRandomVideoFrameNV12()
                }
           }
        }
    
        public func SendRandomVideoFrameNV12() -> Void
        {
            let videoFrameBuffer = GenerateRandomVideoFrameBuffer()
    
            rawOutgoingVideoStream.send(frame: videoFrameBuffer) { error in
                /*Handle error if non-nil*/
            }
    
            let rate = 0.1 / rawOutgoingVideoStream.format.framesPerSecond
            let second: Float = 1000000
            usleep(useconds_t(rate * second))
        }
    
        private func GenerateRandomVideoFrameBuffer() -> RawVideoFrame
        {
            var cvPixelBuffer: CVPixelBuffer? = nil
            guard CVPixelBufferCreate(kCFAllocatorDefault,
                                    rawOutgoingVideoStream.format.width,
                                    rawOutgoingVideoStream.format.height,
                                    kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
                                    nil,
                                    &cvPixelBuffer) == kCVReturnSuccess else {
                fatalError()
            }
    
            GenerateRandomVideoFrameNV12(cvPixelBuffer: cvPixelBuffer!)
    
            CVPixelBufferUnlockBaseAddress(cvPixelBuffer!, .readOnly)
    
            let videoFrameBuffer = RawVideoFrameBuffer()
            videoFrameBuffer.buffer = cvPixelBuffer!
            videoFrameBuffer.streamFormat = rawOutgoingVideoStream.format
    
            return videoFrameBuffer
        }
    
       private func GenerateRandomVideoFrameNV12(cvPixelBuffer: CVPixelBuffer) {
            let w = rawOutgoingVideoStream.format.width
            let h = rawOutgoingVideoStream.format.height
    
            let bufferArrayList = BufferExtensions.getArrayBuffersUnsafe(cvPixelBuffer: cvPixelBuffer)
    
            guard bufferArrayList.count >= 2, let yArrayBuffer = bufferArrayList[0], let uvArrayBuffer = bufferArrayList[1] else {
                return
            }
    
            let yVal = Int32.random(in: 1..<255)
            let uvVal = Int32.random(in: 1..<255)
    
            for y in 0...h
            {
                for x in 0...w
                {
                    yArrayBuffer.storeBytes(of: yVal, toByteOffset: Int((y * w) + x), as: Int32.self)
                }
            }
    
            for y in 0...(h/2)
            {
                for x in 0...(w/2)
                {
                    uvArrayBuffer.storeBytes(of: uvVal, toByteOffset: Int((y * w) + x), as: Int32.self)
                }
            }
        }
    
        public func Start() {
            stopFrameIterator = false
            frameIteratorThread = Thread(target: self, selector: #selector(VideoFrameIterator), object: "VideoFrameSender")
            frameIteratorThread?.start()
        }
    
        public func Stop() {
            if frameIteratorThread != nil {
                stopFrameIterator = true
                frameIteratorThread?.cancel()
                frameIteratorThread = nil
            }
        }
    }
    
  7. Zaimplementuj element w pliku VirtualOutgoingVideoStreamDelegate. Zdarzenie didChangeState informuje o stanie bieżącego strumienia. Nie wysyłaj ramek, jeśli stan nie jest równy VideoStreamState.started.

    /*Delegate Implementer*/ 
    private var videoFrameSender: VideoFrameSender
    func virtualOutgoingVideoStream(
        _ virtualOutgoingVideoStream: VirtualOutgoingVideoStream,
        didChangeState args: VideoStreamStateChangedEventArgs) {
        switch args.stream.state {
            case .available:
                videoFrameSender = VideoFrameSender(rawOutgoingVideoStream)
                break
            case .started:
                /* Start sending frames */
                videoFrameSender.Start()
                break
            case .stopped:
                /* Stop sending frames */
                videoFrameSender.Stop()
                break
        }
    }
    

ScreenShare Video

Ponieważ system Windows generuje ramki, musisz zaimplementować własną usługę pierwszego planu, aby przechwycić ramki i wysłać je przy użyciu interfejsu API wywołującego usługi Azure Communication Services.

Obsługiwane rozwiązania wideo

Współczynnik proporcji Rozwiązanie Maksymalna liczba klatek na sekundę
Cokolwiek Wszystko do 1080p 30

Kroki tworzenia strumienia wideo udziału ekranu

  1. Utwórz tablicę VideoFormat przy użyciu formatu VideoStreamPixelFormat obsługiwanego przez zestaw SDK. Gdy dostępnych jest wiele formatów, kolejność formatów na liście nie ma wpływu na ten format ani nie określa priorytetów używanych formatów. Kryteria wyboru formatu są oparte na czynnikach zewnętrznych, takich jak przepustowość sieci.

    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. Utwórz RawOutgoingVideoStreamOptionselement i ustaw przy VideoFormats użyciu wcześniej utworzonego obiektu.

    var rawOutgoingVideoStreamOptions = RawOutgoingVideoStreamOptions()
    rawOutgoingVideoStreamOptions.formats = videoStreamFormats
    
  3. Utwórz wystąpienie klasy przy użyciu utworzonego RawOutgoingVideoStreamOptions wcześniej wystąpieniaVirtualOutgoingVideoStream.

    var rawOutgoingVideoStream = ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions)
    
  4. Przechwyć i wysłać ramkę wideo w następujący sposób.

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

Nieprzetworzone przychodzące wideo

Ta funkcja zapewnia dostęp do ramek wideo wewnątrz IncomingVideoStreamobiektu w celu manipulowania tymi obiektami strumienia lokalnie

  1. Tworzenie wystąpienia tego IncomingVideoOptions zestawu za pomocą JoinCallOptions ustawienia VideoStreamKind.RawIncoming

    var incomingVideoOptions = IncomingVideoOptions()
    incomingVideoOptions.streamType = VideoStreamKind.rawIncoming
    var joinCallOptions = JoinCallOptions()
    joinCallOptions.incomingVideoOptions = incomingVideoOptions
    
  2. Po otrzymaniu delegata dołączania RemoteParticipant.delegate.didChangedVideoStreamState zdarzeńParticipantsUpdatedEventArgs. To zdarzenie informuje o stanie IncomingVideoStream obiektów.

    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. W tym czasie element IncomingVideoStream ma VideoStreamState.available delegata dołączania RawIncomingVideoStream.delegate.didReceivedRawVideoFrame stanu, jak pokazano w poprzednim kroku. To zdarzenie udostępnia nowe RawVideoFrame obiekty.

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

Jako deweloper możesz uzyskiwać dostęp do nieprzetworzonych multimediów przychodzących i wychodzących zawartości audio, wideo i zawartości ekranu podczas połączenia, aby umożliwić przechwytywanie, analizowanie i przetwarzanie zawartości audio/wideo. Dostęp do nieprzetworzonego dźwięku, nieprzetworzonego wideo i nieprzetworzonego udziału ekranu usług Azure Communication Services zapewnia deweloperom niemal nieograniczoną możliwość wyświetlania i edytowania zawartości audio, wideo i udostępniania zawartości ekranu, która odbywa się w zestawie SDK wywołującym usługi Azure Communication Services. W tym przewodniku Szybki start dowiesz się, jak zaimplementować dostęp do nieprzetworzonych multimediów przy użyciu zestawu SDK wywołującego usługi Azure Communication Services dla języka JavaScript.

Na przykład:

  • Możesz uzyskać dostęp do strumienia audio/wideo połączenia bezpośrednio w obiekcie wywołania i wysyłać niestandardowe wychodzące strumienie audio/wideo podczas połączenia.
  • Możesz sprawdzić strumienie audio i wideo, aby uruchamiać niestandardowe modele sztucznej inteligencji na potrzeby analizy. Takie modele mogą obejmować przetwarzanie języka naturalnego w celu analizowania konwersacji lub udostępniania szczegółowych informacji i sugestii w czasie rzeczywistym w celu zwiększenia produktywności agentów.
  • Organizacje mogą używać strumieni multimediów audio i wideo do analizowania tonacji podczas zapewniania wirtualnej opieki pacjentom lub zapewnienia pomocy zdalnej podczas połączeń wideo korzystających z rzeczywistości mieszanej. Ta funkcja otwiera ścieżkę dla deweloperów, aby zastosować innowacje w celu ulepszenia środowisk interakcji.

Wymagania wstępne

Ważne

Przykłady dostępne są w wersji 1.13.1 zestawu Sdk wywoływania dla języka JavaScript. Pamiętaj, aby użyć tej wersji lub nowszej podczas korzystania z tego przewodnika Szybki start.

Uzyskiwanie dostępu do nieprzetworzonego dźwięku

Uzyskiwanie dostępu do nieprzetworzonych multimediów audio zapewnia dostęp do strumienia audio połączenia przychodzącego oraz możliwość wyświetlania i wysyłania niestandardowych wychodzących strumieni audio podczas połączenia.

Uzyskiwanie dostępu do przychodzącego nieprzetworzonego strumienia audio

Użyj następującego kodu, aby uzyskać dostęp do strumienia audio połączenia przychodzącego.

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

Umieszczanie wywołania za pomocą niestandardowego strumienia audio

Użyj poniższego kodu, aby uruchomić wywołanie z niestandardowym strumieniem audio zamiast używać urządzenia mikrofonu użytkownika.

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

Przełączanie do niestandardowego strumienia audio podczas połączenia

Użyj poniższego kodu, aby przełączyć urządzenie wejściowe na niestandardowy strumień audio zamiast używać urządzenia mikrofonowego użytkownika podczas połączenia.

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

Zatrzymywanie niestandardowego strumienia audio

Użyj poniższego kodu, aby zatrzymać wysyłanie niestandardowego strumienia audio po jego ustawieniu podczas wywołania.

call.stopAudio();

Uzyskiwanie dostępu do nieprzetworzonego wideo

Nieprzetworzone nośniki wideo udostępniają wystąpienie MediaStream obiektu. (Aby uzyskać więcej informacji, zobacz dokumentację języka JavaScript). Nieprzetworzone nośniki wideo umożliwiają dostęp do MediaStream obiektu dla połączeń przychodzących i wychodzących. W przypadku nieprzetworzonego wideo można użyć tego obiektu do stosowania filtrów przy użyciu uczenia maszynowego do przetwarzania ramek wideo.

Przetworzone nieprzetworzone wychodzące klatki wideo można wysyłać jako wychodzące wideo nadawcy. Przetworzone nieprzetworzone przychodzące ramki wideo można renderować po stronie odbiorcy.

Umieszczanie wywołania za pomocą niestandardowego strumienia wideo

Możesz uzyskać dostęp do nieprzetworzonego strumienia wideo dla połączenia wychodzącego. Wychodzący nieprzetworzone strumienie wideo służy MediaStream do przetwarzania ramek przy użyciu uczenia maszynowego i stosowania filtrów. Przetworzone wychodzące wideo można następnie wysłać jako strumień wideo nadawcy.

W tym przykładzie dane kanwy są wysyłane do użytkownika jako wychodzące wideo.

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

Przełączanie do niestandardowego strumienia wideo podczas połączenia

Użyj poniższego kodu, aby przełączyć urządzenie wejściowe na niestandardowy strumień wideo zamiast używać urządzenia aparatu użytkownika podczas wywołania.

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

Zatrzymywanie niestandardowego strumienia wideo

Użyj następującego kodu, aby zatrzymać wysyłanie niestandardowego strumienia wideo po jego ustawieniu podczas wywołania.

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

Podczas przełączania się z aparatu, który ma efekty niestandardowe zastosowane do innego urządzenia aparatu, najpierw zatrzymaj wideo, przełącz źródło na LocalVideoStreamobiekcie i ponownie rozpocznij wideo.

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

Uzyskiwanie dostępu do przychodzącego strumienia wideo od uczestnika zdalnego

Możesz uzyskać dostęp do nieprzetworzonego strumienia wideo dla połączenia przychodzącego. MediaStream Używasz przychodzącego nieprzetworzonego strumienia wideo do przetwarzania ramek przy użyciu uczenia maszynowego i stosowania filtrów. Przetworzone przychodzące wideo można następnie renderować po stronie odbiorcy.

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

Ważne

Ta funkcja usług Azure Communication Services jest obecnie dostępna w wersji zapoznawczej.

Interfejsy API i zestawy SDK w wersji zapoznawczej są udostępniane bez umowy dotyczącej poziomu usług. Zalecamy, aby nie używać ich w przypadku obciążeń produkcyjnych. Niektóre funkcje mogą nie być obsługiwane lub mogą mieć ograniczone możliwości.

Aby uzyskać więcej informacji, zapoznaj się z dodatkowymi warunkami użytkowania dla wersji zapoznawczych platformy Microsoft Azure.

Dostęp do udostępniania nieprzetworzonego ekranu jest dostępny w publicznej wersji zapoznawczej i jest dostępny w wersji 1.15.1-beta.1 lub nowszej.

Uzyskiwanie dostępu do nieprzetworzonego udostępniania ekranu

Nieprzetworzone nośniki udostępniania ekranu umożliwiają dostęp specjalnie do MediaStream obiektu dla przychodzących i wychodzących strumieni udostępniania ekranu. W przypadku udostępniania nieprzetworzonego ekranu można użyć tego obiektu do stosowania filtrów przy użyciu uczenia maszynowego do przetwarzania ramek udziału ekranu.

Przetworzone nieprzetworzone ramki udziału ekranu można wysyłać jako wychodzący udział ekranu nadawcy. Przetworzone nieprzetworzone klatki udziału ekranu przychodzącego można renderować po stronie odbiornika.

Uwaga: wysyłanie udziału ekranu jest obsługiwane tylko w przeglądarce klasycznej.

Udostępnianie ekranu startowego za pomocą niestandardowego strumienia udostępniania ekranu

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'

Uzyskiwanie dostępu do strumienia nieprzetworzonego udostępniania ekranu z poziomu ekranu, karty przeglądarki lub aplikacji oraz stosowanie efektów do strumienia

Poniżej przedstawiono przykład zastosowania czarnego i białego efektu na nieprzetworzonym strumieniu udostępniania ekranu z poziomu ekranu, karty przeglądarki lub aplikacji. UWAGA: Filtr kontekstu kanwy = "grayscale(1)" INTERFEJS API nie jest obsługiwany w przeglądarce Safari.

let bwTimeout;
let bwVideoElem;

const applyBlackAndWhiteEffect = function (stream) {
	let width = 1280, height = 720;
	bwVideoElem = document.createElement("video");
	bwVideoElem.srcObject = stream;
	bwVideoElem.height = height;
	bwVideoElem.width = width;
	bwVideoElem.play();
	const canvas = document.createElement('canvas');
	const bwCtx = canvas.getContext('2d', { willReadFrequently: true });
	canvas.width = width;
	canvas.height = height;
	
	const FPS = 30;
	const processVideo = function () {
	    try {
		let begin = Date.now();
		// start processing.
		// NOTE: The Canvas context filter API is not supported in Safari
		bwCtx.filter = "grayscale(1)";
		bwCtx.drawImage(bwVideoElem, 0, 0, width, height);
		const imageData = bwCtx.getImageData(0, 0, width, height);
		bwCtx.putImageData(imageData, 0, 0);
		// schedule the next one.
		let delay = Math.abs(1000/FPS - (Date.now() - begin));
		bwTimeout = setTimeout(processVideo, delay);
	    } catch (err) {
		console.error(err);
	    }
	}
	
	// schedule the first one.
	bwTimeout = setTimeout(processVideo, 0);
	return canvas.captureStream(FPS);
}

// Call startScreenSharing API without passing any stream parameter. Browser will prompt the user to select the screen, browser tab, or app to share in the call.
await call.startScreenSharing();
const localScreenSharingStream = call.localVideoStreams.find( (stream) => { return stream.mediaStreamType === 'ScreenSharing' });
console.log(localScreenSharingStream.mediaStreamType); // 'ScreenSharing'
// Get the raw media stream from the screen, browser tab, or application
const rawMediaStream = await localScreenSharingStream.getMediaStream();
// Apply effects to the media stream as you wish
const blackAndWhiteMediaStream = applyBlackAndWhiteEffect(rawMediaStream);
// Set the media stream with effects no the local screen sharing stream
await localScreenSharingStream.setMediaStream(blackAndWhiteMediaStream);

// Stop screen sharing and clean up the black and white video filter
await call.stopScreenSharing();
clearTimeout(bwTimeout);
bwVideoElem.srcObject.getVideoTracks().forEach((track) => { track.stop(); });
bwVideoElem.srcObject = null;

Zatrzymywanie wysyłania strumienia udziału ekranu

Użyj poniższego kodu, aby zatrzymać wysyłanie niestandardowego strumienia udziału ekranu po jego ustawieniu podczas wywołania.

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

Uzyskiwanie dostępu do strumienia udziału ekranu przychodzącego od uczestnika zdalnego

Możesz uzyskać dostęp do nieprzetworzonego strumienia udziału ekranu od uczestnika zdalnego. Do przetwarzania ramek przy użyciu uczenia maszynowego i stosowania filtrów używa MediaStream się przychodzącego strumienia nieprzetworzonego udostępniania ekranu. Przetworzony strumień udziału ekranu przychodzącego można następnie renderować po stronie odbiorcy.

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

Następne kroki