التشغيل السريع: إضافة وصول الوسائط الخام إلى تطبيقك

في هذا التشغيل السريع، ستتعلم كيفية تنفيذ الوصول إلى الوسائط الأولية باستخدام Azure Communication Services Calling SDK ل Unity. تقدم Azure Communication Services Calling SDK واجهات برمجة التطبيقات التي تسمح للتطبيقات بإنشاء إطارات الفيديو الخاصة بها لإرسال إطارات الفيديو الأولية أو عرضها من المشاركين عن بعد في مكالمة. يعتمد هذا التشغيل السريع على التشغيل السريع: إضافة مكالمات فيديو 1:1 إلى تطبيقك ل Unity.

الوصول إلى RawVideo

نظرا لأن التطبيق ينشئ إطارات الفيديو، يجب على التطبيق إعلام Azure Communication Services Calling SDK بتنسيقات الفيديو التي يمكن للتطبيق إنشاؤها. تسمح هذه المعلومات ل Azure Communication Services Calling SDK باختيار أفضل تكوين لتنسيق الفيديو لشروط الشبكة في ذلك الوقت.

فيديو ظاهري

دقة الفيديو المدعومة

نسبة العرض إلى الارتفاع نوع الحل الحد الأقصى ل FPS
16x9 1080 بكسل 30
16x9 720 بكسل 30
16x9 540 بكسل 30
16x9 480 بكسل 30
16x9 360 بكسل 30
16x9 270 بكسل 15
16x9 240 بكسل 15
16x9 180 بكسل 15
4x3 VGA (640x480) 30
4x3 424x320 15
4x3 QVGA (320x240) 15
4x3 212x160 15
  1. اتبع الخطوات هنا التشغيل السريع: إضافة مكالمات فيديو 1:1 إلى تطبيقك لإنشاء لعبة Unity. الهدف هو الحصول على كائن CallAgent جاهز لبدء المكالمة. ابحث عن التعليمات البرمجية النهائية لهذا البداية السريعة في GitHub.

  2. إنشاء صفيف VideoFormat من استخدام VideoStreamPixelFormat يدعم SDK. عند توفر تنسيقات متعددة، لا يؤثر ترتيب التنسيقات في القائمة على التنسيق المستخدم أو يحدد أولوياته. تستند معايير تحديد التنسيق إلى عوامل خارجية مثل النطاق الترددي للشبكة.

    var videoStreamFormat = new VideoStreamFormat
    {
        Resolution = VideoStreamResolution.P360, // For VirtualOutgoingVideoStream the width/height should be set using VideoStreamResolution enum
        PixelFormat = VideoStreamPixelFormat.Rgba,
        FramesPerSecond = 15,
        Stride1 = 640 * 4 // It is times 4 because RGBA is a 32-bit format
    };
    VideoStreamFormat[] videoStreamFormats = { videoStreamFormat };
    
  3. إنشاء RawOutgoingVideoStreamOptions، وتعيين Formats مع الكائن الذي تم إنشاؤه مسبقا.

    var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions
    {
        Formats = videoStreamFormats
    };
    
  4. إنشاء مثيل VirtualOutgoingVideoStream باستخدام المثيل RawOutgoingVideoStreamOptions الذي قمت بإنشائه مسبقا.

    var rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  5. اشترك في RawOutgoingVideoStream.FormatChanged المفوض. يعلم هذا الحدث كلما VideoStreamFormat تم تغيير من أحد تنسيقات الفيديو المتوفرة في القائمة.

    rawOutgoingVideoStream.FormatChanged += (object sender, VideoStreamFormatChangedEventArgs args)
    {
        VideoStreamFormat videoStreamFormat = args.Format;
    }
    
  6. اشترك في RawOutgoingVideoStream.StateChanged المفوض. يعلم هذا الحدث كلما State تغير.

    rawOutgoingVideoStream.StateChanged += (object sender, VideoStreamFormatChangedEventArgs args)
    {
        CallVideoStream callVideoStream = e.Stream;
    
        switch (callVideoStream.Direction)
        {
            case StreamDirection.Outgoing:
                OnRawOutgoingVideoStreamStateChanged(callVideoStream as OutgoingVideoStream);
                break;
            case StreamDirection.Incoming:
                OnRawIncomingVideoStreamStateChanged(callVideoStream as IncomingVideoStream);
                break;
        }
    }
    
  7. التعامل مع معاملات حالة دفق الفيديو الصادرة الأولية مثل البدء والإيقاف والبدء في إنشاء إطارات فيديو مخصصة أو تعليق خوارزمية إنشاء الإطار.

    private async void OnRawOutgoingVideoStreamStateChanged(OutgoingVideoStream outgoingVideoStream)
    {
        switch (outgoingVideoStream.State)
        {
            case VideoStreamState.Started:
                switch (outgoingVideoStream.Kind)
                {
                    case VideoStreamKind.VirtualOutgoing:
                        outgoingVideoPlayer.StartGenerateFrames(outgoingVideoStream); // This is where a background worker thread can be started to feed the outgoing video frames.
                        break;
                }
                break;
    
            case VideoStreamState.Stopped:
                switch (outgoingVideoStream.Kind)
                {
                    case VideoStreamKind.VirtualOutgoing:
                        break;
                }
                break;
        }
    }
    

    فيما يلي عينة من منشئ إطار الفيديو الصادر:

    private unsafe RawVideoFrame GenerateRawVideoFrame(RawOutgoingVideoStream rawOutgoingVideoStream)
    {
        var format = rawOutgoingVideoStream.Format;
        int w = format.Width;
        int h = format.Height;
        int rgbaCapacity = w * h * 4;
    
        var rgbaBuffer = new NativeBuffer(rgbaCapacity);
        rgbaBuffer.GetData(out IntPtr rgbaArrayBuffer, out rgbaCapacity);
    
        byte r = (byte)random.Next(1, 255);
        byte g = (byte)random.Next(1, 255);
        byte b = (byte)random.Next(1, 255);
    
        for (int y = 0; y < h; y++)
        {
            for (int x = 0; x < w*4; x += 4)
            {
                ((byte*)rgbaArrayBuffer)[(w * 4 * y) + x + 0] = (byte)(y % r);
                ((byte*)rgbaArrayBuffer)[(w * 4 * y) + x + 1] = (byte)(y % g);
                ((byte*)rgbaArrayBuffer)[(w * 4 * y) + x + 2] = (byte)(y % b);
                ((byte*)rgbaArrayBuffer)[(w * 4 * y) + x + 3] = 255;
            }
        }
    
        // Call ACS Unity SDK API to deliver the frame
        rawOutgoingVideoStream.SendRawVideoFrameAsync(new RawVideoFrameBuffer() {
            Buffers = new NativeBuffer[] { rgbaBuffer },
            StreamFormat = rawOutgoingVideoStream.Format,
            TimestampInTicks = rawOutgoingVideoStream.TimestampInTicks
        }).Wait();
    
        return new RawVideoFrameBuffer()
        {
            Buffers = new NativeBuffer[] { rgbaBuffer },
            StreamFormat = rawOutgoingVideoStream.Format
        };
    }
    

    إشعار

    unsafe يتم استخدام المعدل على هذا الأسلوب حيث NativeBuffer يتطلب الوصول إلى موارد الذاكرة الأصلية. لذلك، Allow unsafe يجب تمكين الخيار في محرر Unity أيضا.

  8. وبالمثل، يمكننا التعامل مع إطارات الفيديو الواردة استجابة لحدث دفق StateChanged الفيديو.

    private void OnRawIncomingVideoStreamStateChanged(IncomingVideoStream incomingVideoStream)
    {
        switch (incomingVideoStream.State)
        {
            case VideoStreamState.Available:
                {
                    var rawIncomingVideoStream = incomingVideoStream as RawIncomingVideoStream;
                    rawIncomingVideoStream.RawVideoFrameReceived += OnRawVideoFrameReceived;
                    rawIncomingVideoStream.Start();
                    break;
                }
            case VideoStreamState.Stopped:
                break;
            case VideoStreamState.NotAvailable:
                break;
        }
    }
    
    private void OnRawVideoFrameReceived(object sender, RawVideoFrameReceivedEventArgs e)
    {
        incomingVideoPlayer.RenderRawVideoFrame(e.Frame);
    }
    
    public void RenderRawVideoFrame(RawVideoFrame rawVideoFrame)
    {
        var videoFrameBuffer = rawVideoFrame as RawVideoFrameBuffer;
        pendingIncomingFrames.Enqueue(new PendingFrame() {
                frame = rawVideoFrame,
                kind = RawVideoFrameKind.Buffer });
    }
    
  9. يوصى بشدة بإدارة كل من إطارات الفيديو الواردة والصادرة من خلال آلية تخزين مؤقت لتجنب التحميل MonoBehaviour.Update() الزائد لطريقة رد الاتصال، والتي يجب أن تظل خفيفة وتجنب مهام وحدة المعالجة المركزية أو الشبكة الثقيلة وضمان تجربة فيديو أكثر سلاسة. يترك هذا التحسين الاختياري للمطورين لتحديد ما يعمل بشكل أفضل في سيناريوهاتهم.

    فيما يلي نموذج لكيفية عرض الإطارات الواردة إلى Unity VideoTexture عن طريق استدعاء Graphics.Blit خارج قائمة انتظار داخلية:

    private void Update()
    {
        if (pendingIncomingFrames.TryDequeue(out PendingFrame pendingFrame))
        {
            switch (pendingFrame.kind)
            {
                case RawVideoFrameKind.Buffer:
                    var videoFrameBuffer = pendingFrame.frame as RawVideoFrameBuffer;
                    VideoStreamFormat videoFormat = videoFrameBuffer.StreamFormat;
                    int width = videoFormat.Width;
                    int height = videoFormat.Height;
                    var texture = new Texture2D(width, height, TextureFormat.RGBA32, mipChain: false);
    
                    var buffers = videoFrameBuffer.Buffers;
                    NativeBuffer buffer = buffers.Count > 0 ? buffers[0] : null;
                    buffer.GetData(out IntPtr bytes, out int signedSize);
    
                    texture.LoadRawTextureData(bytes, signedSize);
                    texture.Apply();
    
                    Graphics.Blit(source: texture, dest: rawIncomingVideoRenderTexture);
                    break;
    
                case RawVideoFrameKind.Texture:
                    break;
            }
            pendingFrame.frame.Dispose();
        }
    }
    

في هذا التشغيل السريع، ستتعلم كيفية تنفيذ الوصول إلى الوسائط الأولية باستخدام Azure Communication Services Calling SDK لنظام التشغيل Windows. تقدم Azure Communication Services Calling SDK واجهات برمجة التطبيقات التي تسمح للتطبيقات بإنشاء إطارات الفيديو الخاصة بها لإرسالها إلى المشاركين عن بعد في مكالمة. يعتمد هذا التشغيل السريع على التشغيل السريع: إضافة مكالمات فيديو 1:1 إلى تطبيقك لنظام التشغيل Windows.

الوصول إلى RawAudio

يمنحك الوصول إلى الوسائط الصوتية البسيطة إمكانية الوصول إلى دفق الصوت للمكالمة الواردة، بالإضافة إلى القدرة على عرض وإرسال تدفقات صوتية صادرة مخصصة أثناء المكالمة.

إرسال صوت صادر خام

قم بعمل عنصر خيارات يحدد خصائص الدفق الخام التي نريد إرسالها.

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

قم بإنشاء RawOutgoingAudioStream وإرفاقه للانضمام إلى خيارات المكالمات ويبدأ الدفق تلقائيا عند توصيل المكالمة.

    JoinCallOptions options =  JoinCallOptions(); // or StartCallOptions()
    OutgoingAudioOptions outgoingAudioOptions = new OutgoingAudioOptions();
    RawOutgoingAudioStream rawOutgoingAudioStream = new RawOutgoingAudioStream(outgoingAudioStreamOptions);
    outgoingAudioOptions.Stream = rawOutgoingAudioStream;
    options.OutgoingAudioOptions = outgoingAudioOptions;
    // Start or Join call with those call options.

إرفاق دفق إلى مكالمة

أو يمكنك أيضا إرفاق الدفق بمثيل موجود Call بدلا من ذلك:

    await call.StartAudio(rawOutgoingAudioStream);

بدء إرسال عينات أولية

يمكننا البدء في إرسال البيانات فقط بمجرد أن تكون حالة الدفق هي AudioStreamState.Started. لمراقبة تغيير حالة دفق الصوت، أضف وحدة استماع إلى OnStateChangedListener الحدث.

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

عند بدء الدفق، يمكننا البدء في إرسال MemoryBuffer عينات صوتية إلى المكالمة. يجب أن يتطابق تنسيق المخزن المؤقت للصوت مع خصائص الدفق المحددة.

    void Start()
    {
        RawOutgoingAudioStreamProperties properties = outgoingAudioStream.Properties;
        RawAudioBuffer buffer;
        new Thread(() =>
        {
            DateTime nextDeliverTime = DateTime.Now;
            while (true)
            {
                MemoryBuffer memoryBuffer = new MemoryBuffer((uint)outgoingAudioStream.ExpectedBufferSizeInBytes);
                using (IMemoryBufferReference reference = memoryBuffer.CreateReference())
                {
                    byte* dataInBytes;
                    uint capacityInBytes;
                    ((IMemoryBufferByteAccess)reference).GetBuffer(out dataInBytes, out capacityInBytes);
                    // Use AudioGraph here to grab data from microphone if you want microphone data
                }
                nextDeliverTime = nextDeliverTime.AddMilliseconds(20);
                buffer = new RawAudioBuffer(memoryBuffer);
                outgoingAudioStream.SendOutgoingAudioBuffer(buffer);
                TimeSpan wait = nextDeliverTime - DateTime.Now;
                if (wait > TimeSpan.Zero)
                {
                    Thread.Sleep(wait);
                }
            }
        }).Start();
    }

تلقي الصوت الوارد الخام

يمكننا أيضا تلقي عينات دفق صوت المكالمات كما MemoryBuffer لو أننا نريد معالجة دفق صوت المكالمة قبل التشغيل. إنشاء كائن RawIncomingAudioStreamOptions يحدد خصائص الدفق الخام التي نريد تلقيها.

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

RawIncomingAudioStream إنشاء وإرفاقه للانضمام إلى خيارات المكالمات

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

أو يمكننا أيضا إرفاق الدفق بمثيل موجود Call بدلا من ذلك:

    await call.startAudio(context, rawIncomingAudioStream);

لبدء تلقي المخازن المؤقتة للصوت الخام من الدفق الوارد، أضف وحدات استماع إلى حالة الدفق الوارد والأحداث المستلمة للمخزن المؤقت.

    unsafe private void OnAudioStateChanged(object sender, AudioStreamStateChanged args)
    {
        if (args.AudioStreamState == AudioStreamState.Started)
        {
            // When value is `AudioStreamState.STARTED` we'll be able to receive samples.
        }
    }
    private void OnRawIncomingMixedAudioBufferAvailable(object sender, IncomingMixedAudioEventArgs args)
    {
        // Received a raw audio buffers(MemoryBuffer).
        using (IMemoryBufferReference reference = args.IncomingAudioBuffer.Buffer.CreateReference())
        {
            byte* dataInBytes;
            uint capacityInBytes;
            ((IMemoryBufferByteAccess)reference).GetBuffer(out dataInBytes, out capacityInBytes);
            // Process the data using AudioGraph class
        }
    }
    rawIncomingAudioStream.StateChanged += OnAudioStateChanged;
    rawIncomingAudioStream.MixedAudioBufferReceived += OnRawIncomingMixedAudioBufferAvailable;

الوصول إلى RawVideo

نظرا لأن التطبيق ينشئ إطارات الفيديو، يجب على التطبيق إعلام Azure Communication Services Calling SDK بتنسيقات الفيديو التي يمكن للتطبيق إنشاؤها. تسمح هذه المعلومات ل Azure Communication Services Calling SDK باختيار أفضل تكوين لتنسيق الفيديو لشروط الشبكة في ذلك الوقت.

فيديو ظاهري

دقة الفيديو المدعومة

نسبة العرض إلى الارتفاع نوع الحل الحد الأقصى ل FPS
16x9 1080 بكسل 30
16x9 720 بكسل 30
16x9 540 بكسل 30
16x9 480 بكسل 30
16x9 360 بكسل 30
16x9 270 بكسل 15
16x9 240 بكسل 15
16x9 180 بكسل 15
4x3 VGA (640x480) 30
4x3 424x320 15
4x3 QVGA (320x240) 15
4x3 212x160 15
  1. إنشاء صفيف VideoFormat من استخدام VideoStreamPixelFormat يدعم SDK. عند توفر تنسيقات متعددة، لا يؤثر ترتيب التنسيقات في القائمة على التنسيق المستخدم أو يحدد أولوياته. تستند معايير تحديد التنسيق إلى عوامل خارجية مثل النطاق الترددي للشبكة.

    var videoStreamFormat = new VideoStreamFormat
    {
        Resolution = VideoStreamResolution.P720, // For VirtualOutgoingVideoStream the width/height should be set using VideoStreamResolution enum
        PixelFormat = VideoStreamPixelFormat.Rgba,
        FramesPerSecond = 30,
        Stride1 = 1280 * 4 // It is times 4 because RGBA is a 32-bit format
    };
    VideoStreamFormat[] videoStreamFormats = { videoStreamFormat };
    
  2. إنشاء RawOutgoingVideoStreamOptions، وتعيين Formats مع الكائن الذي تم إنشاؤه مسبقا.

    var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions
    {
        Formats = videoStreamFormats
    };
    
  3. إنشاء مثيل VirtualOutgoingVideoStream باستخدام المثيل RawOutgoingVideoStreamOptions الذي قمت بإنشائه مسبقا.

    var rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  4. اشترك في RawOutgoingVideoStream.FormatChanged المفوض. يعلم هذا الحدث كلما VideoStreamFormat تم تغيير من أحد تنسيقات الفيديو المتوفرة في القائمة.

    rawOutgoingVideoStream.FormatChanged += (object sender, VideoStreamFormatChangedEventArgs args)
    {
        VideoStreamFormat videoStreamFormat = args.Format;
    }
    
  5. إنشاء مثيل لفئة المساعد التالية للوصول إلى بيانات المخزن المؤقت

    [ComImport]
    [Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    unsafe interface IMemoryBufferByteAccess
    {
        void GetBuffer(out byte* buffer, out uint capacity);
    }
    [ComImport]
    [Guid("905A0FEF-BC53-11DF-8C49-001E4FC686DA")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    unsafe interface IBufferByteAccess
    {
        void Buffer(out byte* buffer);
    }
    internal static class BufferExtensions
    {
        // For accessing MemoryBuffer
        public static unsafe byte* GetArrayBuffer(IMemoryBuffer memoryBuffer)
        {
            IMemoryBufferReference memoryBufferReference = memoryBuffer.CreateReference();
            var memoryBufferByteAccess = memoryBufferReference as IMemoryBufferByteAccess;
            memoryBufferByteAccess.GetBuffer(out byte* arrayBuffer, out uint arrayBufferCapacity);
            GC.AddMemoryPressure(arrayBufferCapacity);
            return arrayBuffer;
        }
        // For accessing MediaStreamSample
        public static unsafe byte* GetArrayBuffer(IBuffer buffer)
        {
            var bufferByteAccess = buffer as IBufferByteAccess;
            bufferByteAccess.Buffer(out byte* arrayBuffer);
            uint arrayBufferCapacity = buffer.Capacity;
            GC.AddMemoryPressure(arrayBufferCapacity);
            return arrayBuffer;
        }
    }
    
  6. إنشاء مثيل لفئة المساعد التالية لإنشاء عشوائي RawVideoFrameباستخدام 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. اشترك في VideoStream.StateChanged المفوض. يعلم هذا الحدث حالة الدفق الحالي. لا ترسل إطارات إذا كانت الحالة لا تساوي VideoStreamState.Started.

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

فيديو مشاركة الشاشة

نظرا لأن نظام Windows ينشئ الإطارات، يجب عليك تنفيذ الخدمة الأمامية الخاصة بك لالتقاط الإطارات وإرسالها باستخدام Azure Communication Services Calling API.

دقة الفيديو المدعومة

نسبة العرض إلى الارتفاع نوع الحل الحد الأقصى ل FPS
أي شيء أي شيء يصل إلى 1080 بكسل 30

خطوات إنشاء دفق فيديو مشاركة شاشة

  1. إنشاء صفيف VideoFormat من استخدام VideoStreamPixelFormat يدعم SDK. عند توفر تنسيقات متعددة، لا يؤثر ترتيب التنسيقات في القائمة على التنسيق المستخدم أو يحدد أولوياته. تستند معايير تحديد التنسيق إلى عوامل خارجية مثل النطاق الترددي للشبكة.
    var videoStreamFormat = new VideoStreamFormat
    {
        Width = 1280, // Width and height can be used for ScreenShareOutgoingVideoStream for custom resolutions or use one of the predefined values inside VideoStreamResolution
        Height = 720,
        //Resolution = VideoStreamResolution.P720,
        PixelFormat = VideoStreamPixelFormat.Rgba,
        FramesPerSecond = 30,
        Stride1 = 1280 * 4 // It is times 4 because RGBA is a 32-bit format.
    };
    VideoStreamFormat[] videoStreamFormats = { videoStreamFormat };
    
  2. إنشاء RawOutgoingVideoStreamOptions، وتعيين VideoFormats مع الكائن الذي تم إنشاؤه مسبقا.
    var rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions
    {
        Formats = videoStreamFormats
    };
    
  3. إنشاء مثيل VirtualOutgoingVideoStream باستخدام المثيل RawOutgoingVideoStreamOptions الذي قمت بإنشائه مسبقا.
    var rawOutgoingVideoStream = new ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  4. التقاط إطار الفيديو وإرساله بالطريقة التالية.
    private async Task SendRawVideoFrame()
    {
        RawVideoFrame videoFrame = null;
        switch (rawVideoFrameKind) //it depends on the frame kind you want to send
        {
            case RawVideoFrameKind.Buffer:
                MemoryBuffer memoryBuffer = // Fill it with the content you got from the Windows APIs
                videoFrame = new RawVideoFrameBuffer()
                {
                    Buffers = memoryBuffer // The number of buffers depends on the VideoStreamPixelFormat
                    StreamFormat = rawOutgoingVideoStream.Format
                };
                break;
            case RawVideoFrameKind.Texture:
                MediaStreamSample mediaStreamSample = // Fill it with the content you got from the Windows APIs
                videoFrame = new RawVideoFrameTexture()
                {
                    Texture = mediaStreamSample, // Texture only receive planar buffers
                    StreamFormat = rawOutgoingVideoStream.Format
                };
                break;
        }
    
        try
        {
            using (videoFrame)
            {
                await rawOutgoingVideoStream.SendRawVideoFrameAsync(videoFrame);
            }
        }
        catch (Exception ex)
        {
            string msg = ex.Message;
        }
    
        try
        {
            int delayBetweenFrames = (int)(1000.0 / rawOutgoingVideoStream.Format.FramesPerSecond);
            await Task.Delay(delayBetweenFrames);
        }
        catch (Exception ex)
        {
            string msg = ex.Message;
        }
    }
    

فيديو وارد أولي

تمنحك هذه الميزة إمكانية الوصول إلى إطارات الفيديو داخل IncomingVideoStream's من أجل معالجة تلك التدفقات محليا

  1. إنشاء مثيل IncomingVideoOptions لتلك المجموعات من خلال JoinCallOptions الإعداد 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. بمجرد تلقي ParticipantsUpdatedEventArgs مفوض إرفاق RemoteParticipant.VideoStreamStateChanged حدث. يعلم هذا الحدث حالة IncomingVideoStream الكائنات.
    private List<RemoteParticipant> remoteParticipantList;
    private void OnRemoteParticipantsUpdated(object sender, ParticipantsUpdatedEventArgs args)
    {
        foreach (RemoteParticipant remoteParticipant in args.AddedParticipants)
        {
            IReadOnlyList<IncomingVideoStream> incomingVideoStreamList = remoteParticipant.IncomingVideoStreams; // Check if there are IncomingVideoStreams already before attaching the delegate
            foreach (IncomingVideoStream incomingVideoStream in incomingVideoStreamList)
            {
                OnRawIncomingVideoStreamStateChanged(incomingVideoStream);
            }
            remoteParticipant.VideoStreamStateChanged += OnVideoStreamStateChanged;
            remoteParticipantList.Add(remoteParticipant); // If the RemoteParticipant ref is not kept alive the VideoStreamStateChanged events are going to be missed
        }
        foreach (RemoteParticipant remoteParticipant in args.RemovedParticipants)
        {
            remoteParticipant.VideoStreamStateChanged -= OnVideoStreamStateChanged;
            remoteParticipantList.Remove(remoteParticipant);
        }
    }
    private void OnVideoStreamStateChanged(object sender, VideoStreamStateChangedEventArgs args)
    {
        CallVideoStream callVideoStream = args.Stream;
        OnRawIncomingVideoStreamStateChanged(callVideoStream as RawIncomingVideoStream);
    }
    private void OnRawIncomingVideoStreamStateChanged(RawIncomingVideoStream rawIncomingVideoStream)
    {
        switch (incomingVideoStream.State)
        {
            case VideoStreamState.Available:
                // There is a new IncomingVideoStream
                rawIncomingVideoStream.RawVideoFrameReceived += OnVideoFrameReceived;
                rawIncomingVideoStream.Start();
                break;
            case VideoStreamState.Started:
                // Will start receiving video frames
                break;
            case VideoStreamState.Stopped:
                // Will stop receiving video frames
                break;
            case VideoStreamState.NotAvailable:
                // The IncomingVideoStream should not be used anymore
                rawIncomingVideoStream.RawVideoFrameReceived -= OnVideoFrameReceived;
                break;
        }
    }
    
  3. في ذلك الوقت، IncomingVideoStream يكون لدى VideoStreamState.Available مفوض إرفاق RawIncomingVideoStream.RawVideoFrameReceived الحالة كما هو موضح في الخطوة السابقة. يوفر ذلك الكائنات الجديدة RawVideoFrame .
    private async void OnVideoFrameReceived(object sender, RawVideoFrameReceivedEventArgs args)
    {
        RawVideoFrame videoFrame = args.Frame;
        switch (videoFrame.Kind) // The type will be whatever was configured on the IncomingVideoOptions
        {
            case RawVideoFrameKind.Buffer:
                // Render/Modify/Save the video frame
                break;
            case RawVideoFrameKind.Texture:
                // Render/Modify/Save the video frame
                break;
        }
    }
    

في هذا التشغيل السريع، ستتعلم كيفية تنفيذ الوصول إلى الوسائط الأولية باستخدام Azure Communication Services Calling SDK لنظام التشغيل Android.

تقدم Azure Communication Services Calling SDK واجهات برمجة التطبيقات التي تسمح للتطبيقات بإنشاء إطارات الفيديو الخاصة بها لإرسالها إلى المشاركين عن بعد في مكالمة.

يعتمد هذا التشغيل السريع على التشغيل السريع: إضافة مكالمات فيديو 1:1 إلى تطبيقك لنظام التشغيل Android.

الوصول إلى RawAudio

يمنحك الوصول إلى الوسائط الصوتية البسيطة إمكانية الوصول إلى دفق الصوت الوارد للمكالمة، بالإضافة إلى القدرة على عرض وإرسال تدفقات الصوت الصادرة المخصصة أثناء المكالمة.

إرسال صوت صادر خام

قم بعمل عنصر خيارات يحدد خصائص الدفق الخام التي نريد إرسالها.

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

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

قم بإنشاء RawOutgoingAudioStream وإرفاقه للانضمام إلى خيارات المكالمات ويبدأ الدفق تلقائيا عند توصيل المكالمة.

    JoinCallOptions options = JoinCallOptions() // or StartCallOptions()

    OutgoingAudioOptions outgoingAudioOptions = new OutgoingAudioOptions();
    RawOutgoingAudioStream rawOutgoingAudioStream = new RawOutgoingAudioStream(outgoingAudioStreamOptions);

    outgoingAudioOptions.setStream(rawOutgoingAudioStream);
    options.setOutgoingAudioOptions(outgoingAudioOptions);

    // Start or Join call with those call options.

إرفاق دفق إلى مكالمة

أو يمكنك أيضا إرفاق الدفق بمثيل موجود Call بدلا من ذلك:

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

بدء إرسال عينات أولية

يمكننا البدء في إرسال البيانات فقط بمجرد أن تكون حالة الدفق هي AudioStreamState.STARTED. لمراقبة تغيير حالة دفق الصوت، أضف وحدة استماع إلى OnStateChangedListener الحدث.

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

    rawOutgoingAudioStream.addOnStateChangedListener(this::onStateChanged)

عند بدء الدفق، يمكننا البدء في إرسال java.nio.ByteBuffer عينات صوتية إلى المكالمة.

يجب أن يتطابق تنسيق المخزن المؤقت للصوت مع خصائص الدفق المحددة.

    Thread thread = new Thread(){
        public void run() {
            RawAudioBuffer buffer;
            Calendar nextDeliverTime = Calendar.getInstance();
            while (true)
            {
                nextDeliverTime.add(Calendar.MILLISECOND, 20);
                byte data[] = new byte[outgoingAudioStream.getExpectedBufferSizeInBytes()];
                //can grab microphone data from AudioRecord
                ByteBuffer dataBuffer = ByteBuffer.allocateDirect(outgoingAudioStream.getExpectedBufferSizeInBytes());
                dataBuffer.rewind();
                buffer = new RawAudioBuffer(dataBuffer);
                outgoingAudioStream.sendOutgoingAudioBuffer(buffer);
                long wait = nextDeliverTime.getTimeInMillis() - Calendar.getInstance().getTimeInMillis();
                if (wait > 0)
                {
                    try {
                        Thread.sleep(wait);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    };
    thread.start();

تلقي الصوت الوارد الخام

يمكننا أيضا تلقي عينات دفق صوت المكالمات كما java.nio.ByteBuffer لو كنا نريد معالجة الصوت قبل التشغيل.

إنشاء كائن RawIncomingAudioStreamOptions يحدد خصائص الدفق الخام التي نريد تلقيها.

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

RawIncomingAudioStream إنشاء وإرفاقه للانضمام إلى خيارات المكالمات

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

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

أو يمكننا أيضا إرفاق الدفق بمثيل موجود Call بدلا من ذلك:


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

لبدء تلقي المخازن المؤقتة للصوت الخام من الدفق الوارد، أضف وحدات استماع إلى حالة الدفق الوارد والأحداث المستلمة للمخزن المؤقت.

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

    private void onMixedAudioBufferReceived(IncomingMixedAudioEvent incomingMixedAudioEvent) {
        // Received a raw audio buffers(java.nio.ByteBuffer).
    }

    rawIncomingAudioStream.addOnStateChangedListener(this::onStateChanged);
    rawIncomingAudioStream.addMixedAudioBufferReceivedListener(this::onMixedAudioBufferReceived);

من المهم أيضا تذكر إيقاف دفق الصوت في مثيل المكالمة Call الحالي:


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

الوصول إلى RawVideo

نظرا لأن التطبيق ينشئ إطارات الفيديو، يجب على التطبيق إعلام Azure Communication Services Calling SDK بتنسيقات الفيديو التي يمكن للتطبيق إنشاؤها. تسمح هذه المعلومات ل Azure Communication Services Calling SDK باختيار أفضل تكوين لتنسيق الفيديو لشروط الشبكة في ذلك الوقت.

فيديو ظاهري

دقة الفيديو المدعومة

نسبة العرض إلى الارتفاع نوع الحل الحد الأقصى ل FPS
16x9 1080 بكسل 30
16x9 720 بكسل 30
16x9 540 بكسل 30
16x9 480 بكسل 30
16x9 360 بكسل 30
16x9 270 بكسل 15
16x9 240 بكسل 15
16x9 180 بكسل 15
4x3 VGA (640x480) 30
4x3 424x320 15
4x3 QVGA (320x240) 15
4x3 212x160 15
  1. إنشاء صفيف VideoFormat من استخدام VideoStreamPixelFormat يدعم SDK.

    عند توفر تنسيقات متعددة، لا يؤثر ترتيب التنسيقات في القائمة على التنسيق المستخدم أو يحدد أولوياته. تستند معايير تحديد التنسيق إلى عوامل خارجية مثل النطاق الترددي للشبكة.

    VideoStreamFormat videoStreamFormat = new VideoStreamFormat();
    videoStreamFormat.setResolution(VideoStreamResolution.P360);
    videoStreamFormat.setPixelFormat(VideoStreamPixelFormat.RGBA);
    videoStreamFormat.setFramesPerSecond(framerate);
    videoStreamFormat.setStride1(w * 4); // It is times 4 because RGBA is a 32-bit format
    
    List<VideoStreamFormat> videoStreamFormats = new ArrayList<>();
    videoStreamFormats.add(videoStreamFormat);
    
  2. إنشاء RawOutgoingVideoStreamOptions، وتعيين Formats مع الكائن الذي تم إنشاؤه مسبقا.

    RawOutgoingVideoStreamOptions rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions();
    rawOutgoingVideoStreamOptions.setFormats(videoStreamFormats);
    
  3. إنشاء مثيل VirtualOutgoingVideoStream باستخدام المثيل RawOutgoingVideoStreamOptions الذي قمت بإنشائه مسبقا.

    VirtualOutgoingVideoStream rawOutgoingVideoStream = new VirtualOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  4. اشترك في RawOutgoingVideoStream.addOnFormatChangedListener المفوض. يعلم هذا الحدث كلما VideoStreamFormat تم تغيير من أحد تنسيقات الفيديو المتوفرة في القائمة.

    virtualOutgoingVideoStream.addOnFormatChangedListener((VideoStreamFormatChangedEvent args) -> 
    {
        VideoStreamFormat videoStreamFormat = args.Format;
    });
    
  5. إنشاء مثيل لفئة المساعد التالية لإنشاء عشوائي RawVideoFrameباستخدام 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. اشترك في VideoStream.addOnStateChangedListener المفوض. يقوم هذا المفوض بإعلام حالة الدفق الحالي. لا ترسل إطارات إذا كانت الحالة لا تساوي VideoStreamState.STARTED.

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

فيديو ScreenShare

نظرا لأن نظام Windows ينشئ الإطارات، يجب عليك تنفيذ الخدمة الأمامية الخاصة بك لالتقاط الإطارات وإرسالها باستخدام Azure Communication Services Calling API.

دقة الفيديو المدعومة

نسبة العرض إلى الارتفاع نوع الحل الحد الأقصى ل FPS
أي شيء أي شيء يصل إلى 1080 بكسل 30

خطوات إنشاء دفق فيديو مشاركة شاشة

  1. إنشاء صفيف VideoFormat من استخدام VideoStreamPixelFormat يدعم SDK.

    عند توفر تنسيقات متعددة، لا يؤثر ترتيب التنسيقات في القائمة على التنسيق المستخدم أو يحدد أولوياته. تستند معايير تحديد التنسيق إلى عوامل خارجية مثل النطاق الترددي للشبكة.

    VideoStreamFormat videoStreamFormat = new VideoStreamFormat();
    videoStreamFormat.setWidth(1280); // Width and height can be used for ScreenShareOutgoingVideoStream for custom resolutions or use one of the predefined values inside VideoStreamResolution
    videoStreamFormat.setHeight(720);
    //videoStreamFormat.setResolution(VideoStreamResolution.P360);
    videoStreamFormat.setPixelFormat(VideoStreamPixelFormat.RGBA);
    videoStreamFormat.setFramesPerSecond(framerate);
    videoStreamFormat.setStride1(w * 4); // It is times 4 because RGBA is a 32-bit format
    
    List<VideoStreamFormat> videoStreamFormats = new ArrayList<>();
    videoStreamFormats.add(videoStreamFormat);
    
  2. إنشاء RawOutgoingVideoStreamOptions، وتعيين VideoFormats مع الكائن الذي تم إنشاؤه مسبقا.

    RawOutgoingVideoStreamOptions rawOutgoingVideoStreamOptions = new RawOutgoingVideoStreamOptions();
    rawOutgoingVideoStreamOptions.setFormats(videoStreamFormats);
    
  3. إنشاء مثيل VirtualOutgoingVideoStream باستخدام المثيل RawOutgoingVideoStreamOptions الذي قمت بإنشائه مسبقا.

    ScreenShareOutgoingVideoStream rawOutgoingVideoStream = new ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions);
    
  4. التقاط إطار الفيديو وإرساله بالطريقة التالية.

    private void SendRawVideoFrame()
    {
        ByteBuffer byteBuffer = // Fill it with the content you got from the Windows APIs
        RawVideoFrameBuffer videoFrame = new RawVideoFrameBuffer();
        videoFrame.setBuffers(Arrays.asList(byteBuffer)); // The number of buffers depends on the VideoStreamPixelFormat
        videoFrame.setStreamFormat(rawOutgoingVideoStream.getFormat());
    
        try
        {
            rawOutgoingVideoStream.sendRawVideoFrame(videoFrame).get();
        }
        catch (Exception ex)
        {
            String msg = ex.getMessage();
        }
        finally
        {
            videoFrame.close();
        }
    }
    

فيديو وارد أولي

تمنحك هذه الميزة إمكانية الوصول إلى إطارات الفيديو داخل IncomingVideoStream الكائنات من أجل معالجة هذه الإطارات محليا

  1. إنشاء مثيل IncomingVideoOptions لتلك المجموعات من خلال JoinCallOptions الإعداد VideoStreamKind.RawIncoming

    IncomingVideoOptions incomingVideoOptions = new IncomingVideoOptions()
            .setStreamType(VideoStreamKind.RAW_INCOMING);
    
    JoinCallOptions joinCallOptions = new JoinCallOptions()
            .setIncomingVideoOptions(incomingVideoOptions);
    
  2. بمجرد تلقي ParticipantsUpdatedEventArgs مفوض إرفاق RemoteParticipant.VideoStreamStateChanged حدث. يعلم هذا الحدث حالة IncomingVideoStream الكائن.

    private List<RemoteParticipant> remoteParticipantList;
    
    private void OnRemoteParticipantsUpdated(ParticipantsUpdatedEventArgs args)
    {
        for (RemoteParticipant remoteParticipant : args.getAddedParticipants())
        {
            List<IncomingVideoStream> incomingVideoStreamList = remoteParticipant.getIncomingVideoStreams(); // Check if there are IncomingVideoStreams already before attaching the delegate
            for (IncomingVideoStream incomingVideoStream : incomingVideoStreamList)
            {
                OnRawIncomingVideoStreamStateChanged(incomingVideoStream);
            }
    
            remoteParticipant.addOnVideoStreamStateChanged(this::OnVideoStreamStateChanged);
            remoteParticipantList.add(remoteParticipant); // If the RemoteParticipant ref is not kept alive the VideoStreamStateChanged events are going to be missed
        }
    
        for (RemoteParticipant remoteParticipant : args.getRemovedParticipants())
        {
            remoteParticipant.removeOnVideoStreamStateChanged(this::OnVideoStreamStateChanged);
            remoteParticipantList.remove(remoteParticipant);
        }
    }
    
    private void OnVideoStreamStateChanged(object sender, VideoStreamStateChangedEventArgs args)
    {
        CallVideoStream callVideoStream = args.getStream();
    
        OnRawIncomingVideoStreamStateChanged((RawIncomingVideoStream) callVideoStream);
    }
    
    private void OnRawIncomingVideoStreamStateChanged(RawIncomingVideoStream rawIncomingVideoStream)
    {
        switch (incomingVideoStream.State)
        {
            case AVAILABLE:
                // There is a new IncomingvideoStream
                rawIncomingVideoStream.addOnRawVideoFrameReceived(this::OnVideoFrameReceived);
                rawIncomingVideoStream.Start();
    
                break;
            case STARTED:
                // Will start receiving video frames
                break;
            case STOPPED:
                // Will stop receiving video frames
                break;
            case NOT_AVAILABLE:
                // The IncomingvideoStream should not be used anymore
                rawIncomingVideoStream.removeOnRawVideoFrameReceived(this::OnVideoFrameReceived);
    
                break;
        }
    }
    
  3. في ذلك الوقت، IncomingVideoStream يكون لدى VideoStreamState.Available مفوض إرفاق RawIncomingVideoStream.RawVideoFrameReceived الحالة كما هو موضح في الخطوة السابقة. يوفر هذا المفوض العناصر الجديدة RawVideoFrame .

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

في هذا التشغيل السريع، ستتعلم كيفية تنفيذ الوصول إلى الوسائط الأولية باستخدام Azure Communication Services Calling SDK لنظام التشغيل iOS.

تقدم Azure Communication Services Calling SDK واجهات برمجة التطبيقات التي تسمح للتطبيقات بإنشاء إطارات الفيديو الخاصة بها لإرسالها إلى المشاركين عن بعد في مكالمة.

يعتمد هذا التشغيل السريع على التشغيل السريع: إضافة مكالمات فيديو 1:1 إلى تطبيقك لنظام التشغيل iOS.

الوصول إلى RawAudio

يمنحك الوصول إلى الوسائط الصوتية البسيطة إمكانية الوصول إلى دفق الصوت للمكالمة الواردة، بالإضافة إلى القدرة على عرض وإرسال تدفقات صوتية صادرة مخصصة أثناء المكالمة.

إرسال صوت صادر خام

قم بعمل عنصر خيارات يحدد خصائص الدفق الخام التي نريد إرسالها.

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

قم بإنشاء RawOutgoingAudioStream وإرفاقه للانضمام إلى خيارات المكالمات ويبدأ الدفق تلقائيا عند توصيل المكالمة.

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

    let outgoingAudioOptions = OutgoingAudioOptions()
    self.rawOutgoingAudioStream = RawOutgoingAudioStream(rawOutgoingAudioStreamOptions: outgoingAudioStreamOptions)
    outgoingAudioOptions.stream = self.rawOutgoingAudioStream
    options.outgoingAudioOptions = outgoingAudioOptions

    // Start or Join call passing the options instance.

إرفاق دفق إلى مكالمة

أو يمكنك أيضا إرفاق الدفق بمثيل موجود Call بدلا من ذلك:


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

بدء إرسال عينات أولية

يمكننا البدء في إرسال البيانات فقط بمجرد أن تكون حالة الدفق هي AudioStreamState.started. لمراقبة تغيير حالة دفق الصوت، نقوم بتنفيذ RawOutgoingAudioStreamDelegate. وقم بتعيينه كمفوض دفق.

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

    self.rawOutgoingAudioStream.delegate = DelegateImplementer()

أو استخدم الإغلاق المستند إلى

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

عند بدء الدفق، يمكننا البدء في إرسال AVAudioPCMBuffer عينات صوتية إلى المكالمة.

يجب أن يتطابق تنسيق المخزن المؤقت للصوت مع خصائص الدفق المحددة.

    protocol SamplesProducer {
        func produceSample(_ currentSample: Int, 
                           options: RawOutgoingAudioStreamOptions) -> AVAudioPCMBuffer
    }

    // Let's use a simple Tone data producer as example.
    // Producing PCM buffers.
    func produceSamples(_ currentSample: Int,
                        stream: RawOutgoingAudioStream,
                        options: RawOutgoingAudioStreamOptions) -> AVAudioPCMBuffer {
        let sampleRate = options.properties.sampleRate
        let channelMode = options.properties.channelMode
        let bufferDuration = options.properties.bufferDuration
        let numberOfChunks = UInt32(1000 / bufferDuration.value)
        let bufferFrameSize = UInt32(sampleRate.valueInHz) / numberOfChunks
        let frequency = 400

        guard let format = AVAudioFormat(commonFormat: .pcmFormatInt16,
                                         sampleRate: sampleRate.valueInHz,
                                         channels: channelMode.channelCount,
                                         interleaved: channelMode == .stereo) else {
            fatalError("Failed to create PCM Format")
        }

        guard let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: bufferFrameSize) else {
            fatalError("Failed to create PCM buffer")
        }

        buffer.frameLength = bufferFrameSize

        let factor: Double = ((2 as Double) * Double.pi) / (sampleRate.valueInHz/Double(frequency))
        var interval = 0
        for sampleIdx in 0..<Int(buffer.frameCapacity * channelMode.channelCount) {
            let sample = sin(factor * Double(currentSample + interval))
            // Scale to maximum amplitude. Int16.max is 37,767.
            let value = Int16(sample * Double(Int16.max))
            
            guard let underlyingByteBuffer = buffer.mutableAudioBufferList.pointee.mBuffers.mData else {
                continue
            }
            underlyingByteBuffer.assumingMemoryBound(to: Int16.self).advanced(by: sampleIdx).pointee = value
            interval += channelMode == .mono ? 2 : 1
        }

        return buffer
    }

    final class RawOutgoingAudioSender {
        let stream: RawOutgoingAudioStream
        let options: RawOutgoingAudioStreamOptions
        let producer: SamplesProducer

        private var timer: Timer?
        private var currentSample: Int = 0
        private var currentTimestamp: Int64 = 0

        init(stream: RawOutgoingAudioStream,
             options: RawOutgoingAudioStreamOptions,
             producer: SamplesProducer) {
            self.stream = stream
            self.options = options
            self.producer = producer
        }

        func start() {
            let properties = self.options.properties
            let interval = properties.bufferDuration.timeInterval

            let channelCount = AVAudioChannelCount(properties.channelMode.channelCount)
            let format = AVAudioFormat(commonFormat: .pcmFormatInt16,
                                       sampleRate: properties.sampleRate.valueInHz,
                                       channels: channelCount,
                                       interleaved: channelCount > 1)!
            self.timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
                guard let self = self else { return }
                let sample = self.producer.produceSamples(self.currentSample, options: self.options)
                let rawBuffer = RawAudioBuffer()
                rawBuffer.buffer = sample
                rawBuffer.timestampInTicks = self.currentTimestamp
                self.stream.send(buffer: rawBuffer, completionHandler: { error in
                    if let error = error {
                        // Handle possible error.
                    }
                })

                self.currentTimestamp += Int64(properties.bufferDuration.value)
                self.currentSample += 1
            }
        }

        func stop() {
            self.timer?.invalidate()
            self.timer = nil
        }

        deinit {
            stop()
        }
    }

من المهم أيضا تذكر إيقاف دفق الصوت في مثيل المكالمة Call الحالي:


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

التقاط عينات الميكروفون

باستخدام Apple، AVAudioEngine يمكننا التقاط إطارات الميكروفون عن طريق الضغط على عقدة إدخال محرك الصوت. التقاط بيانات الميكروفون والقدرة على استخدام وظائف الصوت الخام، يمكننا معالجة الصوت قبل إرساله إلى مكالمة.

    import AVFoundation
    import AzureCommunicationCalling

    enum MicrophoneSenderError: Error {
        case notMatchingFormat
    }

    final class MicrophoneDataSender {
        private let stream: RawOutgoingAudioStream
        private let properties: RawOutgoingAudioStreamProperties
        private let format: AVAudioFormat
        private let audioEngine: AVAudioEngine = AVAudioEngine()

        init(properties: RawOutgoingAudioStreamProperties) throws {
            // This can be different depending on which device we are running or value set for
            // `try AVAudioSession.sharedInstance().setPreferredSampleRate(...)`.
            let nodeFormat = self.audioEngine.inputNode.outputFormat(forBus: 0)
            let matchingSampleRate = AudioSampleRate.allCases.first(where: { $0.valueInHz == nodeFormat.sampleRate })
            guard let inputNodeSampleRate = matchingSampleRate else {
                throw MicrophoneSenderError.notMatchingFormat
            }

            // Override the sample rate to one that matches audio session (Audio engine input node frequency).
            properties.sampleRate = inputNodeSampleRate

            let options = RawOutgoingAudioStreamOptions()
            options.properties = properties

            self.stream = RawOutgoingAudioStream(rawOutgoingAudioStreamOptions: options)
            let channelCount = AVAudioChannelCount(properties.channelMode.channelCount)
            self.format = AVAudioFormat(commonFormat: .pcmFormatInt16,
                                        sampleRate: properties.sampleRate.valueInHz,
                                        channels: channelCount,
                                        interleaved: channelCount > 1)!
            self.properties = properties
        }

        func start() throws {
            guard !self.audioEngine.isRunning else {
                return
            }

            // Install tap documentations states that we can get between 100 and 400 ms of data.
            let framesFor100ms = AVAudioFrameCount(self.format.sampleRate * 0.1)

            // Note that some formats may not be allowed by `installTap`, so we have to specify the 
            // correct properties.
            self.audioEngine.inputNode.installTap(onBus: 0, bufferSize: framesFor100ms, 
                                                  format: self.format) { [weak self] buffer, _ in
                guard let self = self else { return }
                
                let rawBuffer = RawAudioBuffer()
                rawBuffer.buffer = buffer
                // Although we specified either 10ms or 20ms, we allow sending up to 100ms of data
                // as long as it can be evenly divided by the specified size.
                self.stream.send(buffer: rawBuffer) { error in
                    if let error = error {
                        // Handle error
                    }
                }
            }

            try audioEngine.start()
        }

        func stop() {
            audioEngine.stop()
        }
    }

إشعار

معدل عينة عقدة إدخال محرك الصوت افتراضيا >إلى قيمة معدل العينة المفضل لجلسة الصوت المشتركة. لذلك لا يمكننا تثبيت الضغط في تلك العقدة باستخدام قيمة مختلفة. لذلك علينا التأكد من أن معدل عينة الخصائص RawOutgoingStream يطابق المعدل الذي نحصل عليه من الضغط على عينات الميكروفون أو تحويل المخازن المؤقتة للضغط إلى التنسيق الذي يطابق ما هو متوقع في الدفق الصادر.

باستخدام هذه العينة الصغيرة، تعلمنا كيف يمكننا التقاط بيانات الميكروفون AVAudioEngine وإرسال هذه العينات إلى مكالمة باستخدام ميزة الصوت الصادر الخام.

تلقي الصوت الوارد الخام

يمكننا أيضا تلقي عينات دفق صوت المكالمات كما AVAudioPCMBuffer لو كنا نريد معالجة الصوت قبل التشغيل.

إنشاء كائن RawIncomingAudioStreamOptions يحدد خصائص الدفق الخام التي نريد تلقيها.

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

RawOutgoingAudioStream إنشاء وإرفاقه للانضمام إلى خيارات المكالمات

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

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

أو يمكننا أيضا إرفاق الدفق بمثيل موجود Call بدلا من ذلك:


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

لبدء تلقي المخزن المؤقت للصوت الخام من الدفق الوارد، قم بتنفيذ RawIncomingAudioStreamDelegate:

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

    self.rawIncomingStream.delegate = RawIncomingReceiver()

أو

    rawIncomingAudioStream.events.mixedAudioBufferReceived = { args in
        // Receive raw audio buffers(AVAudioPCMBuffer) and process them using AVAudioEngine API's.
    }

    rawIncomingAudioStream.events.onStateChanged = { args in
        // To be notified when stream started and stopped.
    }

الوصول إلى RawVideo

نظرا لأن التطبيق ينشئ إطارات الفيديو، يجب على التطبيق إعلام Azure Communication Services Calling SDK بتنسيقات الفيديو التي يمكن للتطبيق إنشاؤها. تسمح هذه المعلومات ل Azure Communication Services Calling SDK باختيار أفضل تكوين لتنسيق الفيديو لشروط الشبكة في ذلك الوقت.

فيديو ظاهري

دقة الفيديو المدعومة

نسبة العرض إلى الارتفاع نوع الحل الحد الأقصى ل FPS
16x9 1080 بكسل 30
16x9 720 بكسل 30
16x9 540 بكسل 30
16x9 480 بكسل 30
16x9 360 بكسل 30
16x9 270 بكسل 15
16x9 240 بكسل 15
16x9 180 بكسل 15
4x3 VGA (640x480) 30
4x3 424x320 15
4x3 QVGA (320x240) 15
4x3 212x160 15
  1. إنشاء صفيف VideoFormat من استخدام VideoStreamPixelFormat يدعم SDK. عند توفر تنسيقات متعددة، لا يؤثر ترتيب التنسيقات في القائمة على التنسيق المستخدم أو يحدد أولوياته. تستند معايير تحديد التنسيق إلى عوامل خارجية مثل النطاق الترددي للشبكة.

    var videoStreamFormat = VideoStreamFormat()
    videoStreamFormat.resolution = VideoStreamResolution.p360
    videoStreamFormat.pixelFormat = VideoStreamPixelFormat.nv12
    videoStreamFormat.framesPerSecond = framerate
    videoStreamFormat.stride1 = w // w is the resolution width
    videoStreamFormat.stride2 = w / 2 // w is the resolution width
    
    var videoStreamFormats: [VideoStreamFormat] = [VideoStreamFormat]()
    videoStreamFormats.append(videoStreamFormat)
    
  2. إنشاء RawOutgoingVideoStreamOptions، وتعيين التنسيقات باستخدام الكائن الذي تم إنشاؤه مسبقا.

    var rawOutgoingVideoStreamOptions = RawOutgoingVideoStreamOptions()
    rawOutgoingVideoStreamOptions.formats = videoStreamFormats
    
  3. إنشاء مثيل VirtualOutgoingVideoStream باستخدام المثيل RawOutgoingVideoStreamOptions الذي قمت بإنشائه مسبقا.

    var rawOutgoingVideoStream = VirtualOutgoingVideoStream(videoStreamOptions: rawOutgoingVideoStreamOptions)
    
  4. تنفيذ إلى VirtualOutgoingVideoStreamDelegate المفوض. didChangeFormat يتم إعلام الحدث كلما VideoStreamFormat تم تغيير من أحد تنسيقات الفيديو المتوفرة في القائمة.

    virtualOutgoingVideoStream.delegate = /* Attach delegate and implement didChangeFormat */
    
  5. إنشاء مثيل لفئة المساعد التالية للوصول إلى CVPixelBuffer البيانات

    final class BufferExtensions: NSObject {
        public static func getArrayBuffersUnsafe(cvPixelBuffer: CVPixelBuffer) -> Array<UnsafeMutableRawPointer?>
        {
            var bufferArrayList: Array<UnsafeMutableRawPointer?> = [UnsafeMutableRawPointer?]()
    
            let cvStatus: CVReturn = CVPixelBufferLockBaseAddress(cvPixelBuffer, .readOnly)
    
            if cvStatus == kCVReturnSuccess {
                let bufferListSize = CVPixelBufferGetPlaneCount(cvPixelBuffer);
                for i in 0...bufferListSize {
                    let bufferRef = CVPixelBufferGetBaseAddressOfPlane(cvPixelBuffer, i)
                    bufferArrayList.append(bufferRef)
                }
            }
    
            return bufferArrayList
        }
    }
    
  6. إنشاء مثيل لفئة المساعد التالية لإنشاء عشوائي RawVideoFrameBufferباستخدام 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. نفذ إلى VirtualOutgoingVideoStreamDelegate. يعلم didChangeState الحدث حالة الدفق الحالي. لا ترسل إطارات إذا كانت الحالة لا تساوي VideoStreamState.started.

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

فيديو ScreenShare

نظرا لأن نظام Windows ينشئ الإطارات، يجب عليك تنفيذ الخدمة الأمامية الخاصة بك لالتقاط الإطارات وإرسالها باستخدام Azure Communication Services Calling API.

دقة الفيديو المدعومة

نسبة العرض إلى الارتفاع نوع الحل الحد الأقصى ل FPS
أي شيء أي شيء يصل إلى 1080 بكسل 30

خطوات إنشاء دفق فيديو مشاركة شاشة

  1. إنشاء صفيف VideoFormat من استخدام VideoStreamPixelFormat يدعم SDK. عند توفر تنسيقات متعددة، لا يؤثر ترتيب التنسيقات في القائمة على التنسيق المستخدم أو يحدد أولوياته. تستند معايير تحديد التنسيق إلى عوامل خارجية مثل النطاق الترددي للشبكة.

    let videoStreamFormat = VideoStreamFormat()
    videoStreamFormat.width = 1280 /* Width and height can be used for ScreenShareOutgoingVideoStream for custom resolutions or use one of the predefined values inside VideoStreamResolution */
    videoStreamFormat.height = 720
    /*videoStreamFormat.resolution = VideoStreamResolution.p360*/
    videoStreamFormat.pixelFormat = VideoStreamPixelFormat.rgba
    videoStreamFormat.framesPerSecond = framerate
    videoStreamFormat.stride1 = w * 4 /* It is times 4 because RGBA is a 32-bit format */
    
    var videoStreamFormats: [VideoStreamFormat] = []
    videoStreamFormats.append(videoStreamFormat)
    
  2. إنشاء RawOutgoingVideoStreamOptions، وتعيين VideoFormats مع الكائن الذي تم إنشاؤه مسبقا.

    var rawOutgoingVideoStreamOptions = RawOutgoingVideoStreamOptions()
    rawOutgoingVideoStreamOptions.formats = videoStreamFormats
    
  3. إنشاء مثيل VirtualOutgoingVideoStream باستخدام المثيل RawOutgoingVideoStreamOptions الذي قمت بإنشائه مسبقا.

    var rawOutgoingVideoStream = ScreenShareOutgoingVideoStream(rawOutgoingVideoStreamOptions)
    
  4. التقاط إطار الفيديو وإرساله بالطريقة التالية.

    private func SendRawVideoFrame() -> Void
    {
        CVPixelBuffer cvPixelBuffer = /* Fill it with the content you got from the Windows APIs, The number of buffers depends on the VideoStreamPixelFormat */
        let videoFrameBuffer = RawVideoFrameBuffer()
        videoFrameBuffer.buffer = cvPixelBuffer!
        videoFrameBuffer.streamFormat = rawOutgoingVideoStream.format
    
        rawOutgoingVideoStream.send(frame: videoFrame) { error in
            /*Handle error if not nil*/
        }
    }
    

فيديو وارد أولي

تمنحك هذه الميزة إمكانية الوصول إلى إطارات الفيديو داخل IncomingVideoStream's من أجل معالجة كائنات الدفق هذه محليا

  1. إنشاء مثيل IncomingVideoOptions لتلك المجموعات من خلال JoinCallOptions الإعداد VideoStreamKind.RawIncoming

    var incomingVideoOptions = IncomingVideoOptions()
    incomingVideoOptions.streamType = VideoStreamKind.rawIncoming
    var joinCallOptions = JoinCallOptions()
    joinCallOptions.incomingVideoOptions = incomingVideoOptions
    
  2. بمجرد تلقي ParticipantsUpdatedEventArgs مفوض إرفاق RemoteParticipant.delegate.didChangedVideoStreamState حدث. يعلم هذا الحدث حالة IncomingVideoStream الكائنات.

    private var remoteParticipantList: [RemoteParticipant] = []
    
    func call(_ call: Call, didUpdateRemoteParticipant args: ParticipantsUpdatedEventArgs) {
        args.addedParticipants.forEach { remoteParticipant in
            remoteParticipant.incomingVideoStreams.forEach { incomingVideoStream in
                OnRawIncomingVideoStreamStateChanged(incomingVideoStream: incomingVideoStream)
            }
            remoteParticipant.delegate = /* Attach delegate OnVideoStreamStateChanged*/
        }
    
        args.removedParticipants.forEach { remoteParticipant in
            remoteParticipant.delegate = nil
        }
    }
    
    func remoteParticipant(_ remoteParticipant: RemoteParticipant, 
                           didVideoStreamStateChanged args: VideoStreamStateChangedEventArgs) {
        OnRawIncomingVideoStreamStateChanged(rawIncomingVideoStream: args.stream)
    }
    
    func OnRawIncomingVideoStreamStateChanged(rawIncomingVideoStream: RawIncomingVideoStream) {
        switch incomingVideoStream.state {
            case .available:
                /* There is a new IncomingVideoStream */
                rawIncomingVideoStream.delegate /* Attach delegate OnVideoFrameReceived*/
                rawIncomingVideoStream.start()
                break;
            case .started:
                /* Will start receiving video frames */
                break
            case .stopped:
                /* Will stop receiving video frames */
                break
            case .notAvailable:
                /* The IncomingVideoStream should not be used anymore */
                rawIncomingVideoStream.delegate = nil
                break
        }
    }
    
  3. في ذلك الوقت، IncomingVideoStream يكون لدى VideoStreamState.available مفوض إرفاق RawIncomingVideoStream.delegate.didReceivedRawVideoFrame الحالة كما هو موضح في الخطوة السابقة. يوفر هذا الحدث العناصر الجديدة RawVideoFrame .

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

بصفتك مطورا، يمكنك الوصول إلى الوسائط الأولية لمحتوى مشاركة الصوت والفيديو والشاشة الوارد والصادر أثناء المكالمة بحيث يمكنك التقاط محتوى الصوت/الفيديو وتحليله ومعالجته. يمنح الوصول إلى الصوت الخام والفيديو الخام ومشاركة الشاشة الأولية من جانب العميل في Azure Communication Services المطورين قدرة غير محدودة تقريبا على عرض وتحرير محتوى مشاركة الصوت والفيديو والشاشة الذي يحدث داخل Azure Communication Services Calling SDK. في هذا التشغيل السريع، ستتعلم كيفية تنفيذ الوصول إلى الوسائط الأولية باستخدام Azure Communication Services Calling SDK ل JavaScript.

على سبيل المثال،

  • يمكنك الوصول إلى دفق الصوت/الفيديو الخاص بالمكالمة مباشرة على كائن المكالمة وإرسال تدفقات صوت/فيديو صادرة مخصصة أثناء المكالمة.
  • يمكنك فحص تدفقات الصوت والفيديو لتشغيل نماذج الذكاء الاصطناعي المخصصة للتحليل. قد تتضمن هذه النماذج معالجة اللغة الطبيعية لتحليل المحادثات أو لتوفير رؤى واقتراحات في الوقت الحقيقي لتعزيز إنتاجية العامل.
  • يمكن للمؤسسات استخدام تدفقات وسائط الصوت والفيديو لتحليل التوجه عند توفير الرعاية الافتراضية للمرضى أو لتوفير المساعدة عن بعد أثناء مكالمات الفيديو التي تستخدم الحقيقة المختلطة. تفتح هذه الإمكانية مسارا للمطورين لتطبيق الابتكارات لتعزيز تجارب التفاعل.

المتطلبات الأساسية

هام

تتوفر الأمثلة هنا في 1.13.1 من استدعاء SDK ل JavaScript. تأكد من استخدام هذا الإصدار أو الإصدارات الأحدث عند تجربة هذا التشغيل السريع.

الوصول إلى الصوت الخام

يمنحك الوصول إلى الوسائط الصوتية البسيطة إمكانية الوصول إلى دفق الصوت للمكالمة الواردة، بالإضافة إلى القدرة على عرض وإرسال تدفقات صوتية صادرة مخصصة أثناء المكالمة.

الوصول إلى دفق صوت خام وارد

استخدم التعليمات البرمجية التالية للوصول إلى دفق صوت مكالمة واردة.

const userId = 'acs_user_id';
const call = callAgent.startCall(userId);
const callStateChangedHandler = async () => {
    if (call.state === "Connected") {
        const remoteAudioStream = call.remoteAudioStreams[0];
        const mediaStream = await remoteAudioStream.getMediaStream();
	// process the incoming call's audio media stream track
    }
};

callStateChangedHandler();
call.on("stateChanged", callStateChangedHandler);

إجراء مكالمة باستخدام دفق صوت مخصص

استخدم التعليمات البرمجية التالية لبدء مكالمة مع دفق صوت مخصص بدلا من استخدام جهاز ميكروفون المستخدم.

const createBeepAudioStreamToSend = () => {
    const context = new AudioContext();
    const dest = context.createMediaStreamDestination();
    const os = context.createOscillator();
    os.type = 'sine';
    os.frequency.value = 500;
    os.connect(dest);
    os.start();
    const { stream } = dest;
    return stream;
};

...
const userId = 'acs_user_id';
const mediaStream = createBeepAudioStreamToSend();
const localAudioStream = new LocalAudioStream(mediaStream);
const callOptions = {
    audioOptions: {
        localAudioStreams: [localAudioStream]
    }
};
callAgent.startCall(userId, callOptions);

التبديل إلى دفق صوت مخصص أثناء إجراء مكالمة

استخدم التعليمات البرمجية التالية لتبديل جهاز إدخال إلى دفق صوت مخصص بدلا من استخدام جهاز ميكروفون المستخدم أثناء المكالمة.

const createBeepAudioStreamToSend = () => {
    const context = new AudioContext();
    const dest = context.createMediaStreamDestination();
    const os = context.createOscillator();
    os.type = 'sine';
    os.frequency.value = 500;
    os.connect(dest);
    os.start();
    const { stream } = dest;
    return stream;
};

...

const userId = 'acs_user_id';
const mediaStream = createBeepAudioStreamToSend();
const localAudioStream = new LocalAudioStream(mediaStream);
const call = callAgent.startCall(userId);
const callStateChangedHandler = async () => {
    if (call.state === 'Connected') {
        await call.startAudio(localAudioStream);
    }
};

callStateChangedHandler();
call.on('stateChanged', callStateChangedHandler);

إيقاف دفق صوت مخصص

استخدم التعليمات البرمجية التالية لإيقاف إرسال دفق صوت مخصص بعد تعيينه أثناء المكالمة.

call.stopAudio();

الوصول إلى الفيديو البسيط

تمنحك وسائط الفيديو الأولية مثيل كائن MediaStream . (لمزيد من المعلومات، راجع وثائق JavaScript.) تمنح وسائط الفيديو الأولية حق الوصول على وجه التحديد إلى MediaStream الكائن للمكالمات الواردة والصادرة. بالنسبة إلى الفيديو البسيط، يمكنك استخدام هذا الكائن لتطبيق عوامل التصفية باستخدام التعلم الآلي لمعالجة إطارات الفيديو.

يمكن إرسال إطارات الفيديو الصادرة الأولية المعالجة كفيديو صادر للمرسل. يمكن عرض إطارات الفيديو الواردة الأولية المعالجة على جانب المتلقي.

إجراء مكالمة باستخدام دفق فيديو مخصص

يمكنك الوصول إلى دفق الفيديو الخام لمكالمة صادرة. يمكنك استخدام MediaStream لدفق الفيديو الخام الصادر لمعالجة الإطارات باستخدام التعلم الآلي وتطبيق عوامل التصفية. يمكن بعد ذلك إرسال الفيديو الصادر المعالج كتدفق فيديو مرسل.

يرسل هذا المثال بيانات اللوحة إلى مستخدم كفيديو صادر.

const createVideoMediaStreamToSend = () => {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    canvas.width = 1500;
    canvas.height = 845;
    ctx.fillStyle = 'blue';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    const colors = ['red', 'yellow', 'green'];
    window.setInterval(() => {
        if (ctx) {
            ctx.fillStyle = colors[Math.floor(Math.random() * colors.length)];
            const x = Math.floor(Math.random() * canvas.width);
            const y = Math.floor(Math.random() * canvas.height);
            const size = 100;
            ctx.fillRect(x, y, size, size);
        }
    }, 1000 / 30);

    return canvas.captureStream(30);
};

...
const userId = 'acs_user_id';
const mediaStream = createVideoMediaStreamToSend();
const localVideoStream = new LocalVideoStream(mediaStream);
const callOptions = {
    videoOptions: {
        localVideoStreams: [localVideoStream]
    }
};
callAgent.startCall(userId, callOptions);

التبديل إلى دفق فيديو مخصص أثناء مكالمة

استخدم التعليمات البرمجية التالية لتبديل جهاز إدخال إلى دفق فيديو مخصص بدلا من استخدام جهاز كاميرا المستخدم أثناء المكالمة.

const createVideoMediaStreamToSend = () => {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    canvas.width = 1500;
    canvas.height = 845;
    ctx.fillStyle = 'blue';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    const colors = ['red', 'yellow', 'green'];
    window.setInterval(() => {
        if (ctx) {
            ctx.fillStyle = colors[Math.floor(Math.random() * colors.length)];
            const x = Math.floor(Math.random() * canvas.width);
            const y = Math.floor(Math.random() * canvas.height);
            const size = 100;
            ctx.fillRect(x, y, size, size);
	 }
    }, 1000 / 30);

    return canvas.captureStream(30);
};

...

const userId = 'acs_user_id';
const call = callAgent.startCall(userId);
const callStateChangedHandler = async () => {
    if (call.state === 'Connected') {    	
        const mediaStream = createVideoMediaStreamToSend();
        const localVideoStream = this.call.localVideoStreams.find((stream) => { return stream.mediaStreamType === 'Video' });
        await localVideoStream.setMediaStream(mediaStream);
    }
};

callStateChangedHandler();
call.on('stateChanged', callStateChangedHandler);

إيقاف دفق فيديو مخصص

استخدم التعليمات البرمجية التالية لإيقاف إرسال دفق فيديو مخصص بعد تعيينه أثناء المكالمة.

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

عند التبديل من كاميرا لها تأثيرات مخصصة مطبقة على جهاز كاميرا آخر، قم أولا بإيقاف الفيديو، وقم بتبديل المصدر على LocalVideoStream، وبدء تشغيل الفيديو مرة أخرى.

const cameras = await this.deviceManager.getCameras();
const newCameraDeviceInfo = cameras.find(cameraDeviceInfo => { return cameraDeviceInfo.id === '<another camera that you want to switch to>' });
// If current camera is using custom raw media stream and video is on
if (this.localVideoStream.mediaStreamType === 'RawMedia' && this.state.videoOn) {
	// Stop raw custom video first
	await this.call.stopVideo(this.localVideoStream);
	// Switch the local video stream's source to the new camera to use
	this.localVideoStream?.switchSource(newCameraDeviceInfo);
	// Start video with the new camera device
	await this.call.startVideo(this.localVideoStream);

// Else if current camera is using normal stream from camera device and video is on
} else if (this.localVideoStream.mediaStreamType === 'Video' && this.state.videoOn) {
	// You can just switch the source, no need to stop and start again. Sent video will automatically switch to the new camera to use
	this.localVideoStream?.switchSource(newCameraDeviceInfo);
}

الوصول إلى دفق الفيديو الوارد من مشارك بعيد

يمكنك الوصول إلى دفق الفيديو الخام لمكالمة واردة. يمكنك استخدام MediaStream لدفق الفيديو الخام الوارد لمعالجة الإطارات باستخدام التعلم الآلي وتطبيق عوامل التصفية. يمكن بعد ذلك عرض الفيديو الوارد المعالج على جانب المتلقي.

const remoteVideoStream = remoteParticipants[0].videoStreams.find((stream) => { return stream.mediaStreamType === 'Video'});
const processMediaStream = async () => {
    if (remoteVideoStream.isAvailable) {
	// remote video stream is turned on, process the video's raw media stream.
	const mediaStream = await remoteVideoStream.getMediaStream();
    } else {
	// remote video stream is turned off, handle it
    }
};

remoteVideoStream.on('isAvailableChanged', async () => {
    await processMediaStream();
});

await processMediaStream();

هام

هذه الميزة من Azure Communication Services قيد المعاينة حاليا.

يتم توفير واجهات برمجة التطبيقات وSDKs للمعاينة دون اتفاقية على مستوى الخدمة. نوصي بعدم استخدامها لأحمال عمل الإنتاج. قد لا تكون بعض الميزات مدعومة، أو قد تكون لها قدرات مقيدة.

لمزيد من المعلومات، راجع شروط الاستخدام التكميلية لمعاينات Microsoft Azure.

يتوفر الوصول إلى مشاركة الشاشة الأولية في المعاينة العامة ويتوفر كجزء من الإصدار 1.15.1-beta.1+.

الوصول إلى مشاركة الشاشة الأولية

تمنح وسائط مشاركة الشاشة الأولية الوصول إلى العنصر خصيصا MediaStream لتدفقات مشاركة الشاشة الواردة والصادرة. لمشاركة الشاشة الأولية، يمكنك استخدام هذا الكائن لتطبيق عوامل التصفية باستخدام التعلم الآلي لمعالجة إطارات مشاركة الشاشة.

يمكن إرسال إطارات مشاركة الشاشة الأولية المعالجة كمشاركة شاشة صادرة للمرسل. يمكن عرض إطارات مشاركة الشاشة الواردة الأولية المعالجة على جانب المتلقي.

ملاحظة: يتم دعم إرسال مشاركة الشاشة فقط على مستعرض سطح المكتب.

بدء مشاركة الشاشة مع دفق مشاركة شاشة مخصص

const createVideoMediaStreamToSend = () => {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    canvas.width = 1500;
    canvas.height = 845;
    ctx.fillStyle = 'blue';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    const colors = ['red', 'yellow', 'green'];
    window.setInterval(() => {
        if (ctx) {
            ctx.fillStyle = colors[Math.floor(Math.random() * colors.length)];
            const x = Math.floor(Math.random() * canvas.width);
            const y = Math.floor(Math.random() * canvas.height);
            const size = 100;
            ctx.fillRect(x, y, size, size);
        }
    }, 1000 / 30);

    return canvas.captureStream(30);
};

...
const mediaStream = createVideoMediaStreamToSend();
const localScreenSharingStream = new LocalVideoStream(mediaStream);
// Will start screen sharing with custom raw media stream
await call.startScreenSharing(localScreenSharingStream);
console.log(localScreenSharingStream.mediaStreamType) // 'RawMedia'

الوصول إلى دفق مشاركة الشاشة الخام من شاشة أو علامة تبويب مستعرض أو تطبيق وتطبيق التأثيرات على الدفق

فيما يلي مثال على كيفية تطبيق تأثير أسود وأبيض على دفق مشاركة الشاشة الخام من شاشة أو علامة تبويب مستعرض أو تطبيق. ملاحظة: عامل تصفية سياق Canvas = "grayscale(1)" API غير مدعوم على Safari.

let bwTimeout;
let bwVideoElem;

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

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

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

إيقاف إرسال دفق مشاركة الشاشة

استخدم التعليمات البرمجية التالية لإيقاف إرسال دفق مشاركة شاشة مخصص بعد تعيينه أثناء المكالمة.

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

الوصول إلى دفق مشاركة الشاشة الواردة من مشارك بعيد

يمكنك الوصول إلى دفق مشاركة الشاشة الخام من مشارك بعيد. يمكنك استخدام MediaStream لدفق مشاركة الشاشة الخام الوارد لمعالجة الإطارات باستخدام التعلم الآلي وتطبيق عوامل التصفية. يمكن بعد ذلك عرض دفق مشاركة الشاشة الواردة المعالجة على جانب المتلقي.

const remoteScreenSharingStream = remoteParticipants[0].videoStreams.find((stream) => { return stream.mediaStreamType === 'ScreenSharing'});
const processMediaStream = async () => {
    if (remoteScreenSharingStream.isAvailable) {
	// remote screen sharing stream is turned on, process the stream's raw media stream.
	const mediaStream = await remoteScreenSharingStream.getMediaStream();
    } else {
	// remote video stream is turned off, handle it
    }
};

remoteScreenSharingStream.on('isAvailableChanged', async () => {
    await processMediaStream();
});

await processMediaStream();

الخطوات التالية

لمزيد من المعلومات، راجع المقالات التالية: