Android平台RTMP推送|轻量级RTSP服务能力封装代码实现

​好多开发者问我们,有没有针对Android平台RTMP直播推送、轻量级RTSP服务模块的进一步封装,可以更便捷的调用大牛直播SDK接口。

为此,我们分享下我们针对Android平台SmartPublisher做的二次封装代码:

ini 复制代码
package com.daniulive.smartpublisher;

import android.util.Log;

import java.nio.ByteBuffer;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class LibPublisherWrapper {
    private static String TAG = "NTLogLibPBW";
    private static final int OK = 0;

    private final ReadWriteLock rw_lock_ = new ReentrantReadWriteLock(true);
    private final java.util.concurrent.locks.Lock write_lock_ = rw_lock_.writeLock();
    private final java.util.concurrent.locks.Lock read_lock_ = rw_lock_.readLock();

    private SmartPublisherJniV2 lib_publisher_;
    private volatile long native_handle_;

    private volatile boolean is_rtmp_publishing_;
    private volatile boolean is_recording_;
    private volatile boolean is_rtsp_publishing_;
    private volatile boolean is_gb_stream_publishing_;

    private void clear_all_publishing_flags() {
        this.is_rtmp_publishing_ = false;
        this.is_recording_ = false;
        this.is_rtsp_publishing_ = false;
        this.is_gb_stream_publishing_ = false;
    }

    public void set(SmartPublisherJniV2 lib_publisher, long handle) {
        if (!empty())
            throw new IllegalStateException("it is not empty");

        if (handle != 0) {
            if (null == lib_publisher)
                throw new NullPointerException("lib_publisher is null");
        }

        write_lock_.lock();
        try {
            clear_all_publishing_flags();
            this.lib_publisher_ = lib_publisher;
            this.native_handle_ = handle;
        } finally {
            write_lock_.unlock();
        }

        Log.i(TAG, "set native_handle:" + handle);
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            if (check_native_handle()) {
                if(is_rtmp_publishing()) {
                    lib_publisher_.SmartPublisherStopPublisher(get());
                    this.is_rtmp_publishing_ = false;
                }

                if(is_recording()) {
                    lib_publisher_.SmartPublisherStopRecorder(get());
                    this.is_recording_ = false;
                }

                if (is_rtsp_publishing()) {
                    lib_publisher_.StopRtspStream(get());
                    this.is_rtsp_publishing_ = false;
                }

                if (is_gb_stream_publishing()) {
                    lib_publisher_.StopGB28181MediaStream(get());
                    this.is_gb_stream_publishing_ = false;
                }

                lib_publisher_.SmartPublisherClose(this.native_handle_);
                Log.i(TAG, "finalize close handle:" + this.native_handle_);
                this.native_handle_ = 0;
            }
        }catch (Exception e) {

        }

        super.finalize();
    }

    public void release() {
        if (empty())
            return;

        if(is_rtmp_publishing())
            StopPublisher();

        if (is_recording())
            StopRecorder();

        if (is_rtsp_publishing())
            StopRtspStream();

        if (is_gb_stream_publishing())
            StopGB28181MediaStream();

        long handle;
        write_lock_.lock();
        try {
            handle = this.native_handle_;
            this.native_handle_ = 0;
            clear_all_publishing_flags();
        } finally {
            write_lock_.unlock();
        }

        if (lib_publisher_ != null && handle != 0)
            lib_publisher_.SmartPublisherClose(handle);
    }

    public boolean try_release() {
        if (empty())
            return false;

        if (is_publishing()) {
            Log.i(TAG, "try_release it is publishing, native_handle:" + get());
            return false;
        }

        long handle;
        write_lock_.lock();
        try {
            if (is_publishing())
                return false;

            handle = this.native_handle_;
            this.native_handle_ = 0;
        } finally {
            write_lock_.unlock();
        }

        if (lib_publisher_ != null && handle != 0)
            lib_publisher_.SmartPublisherClose(handle);

        return true;
    }

    public final boolean empty() { return 0 == this.native_handle_; }

    private final long get() { return this.native_handle_; }

    private final boolean check_native_handle() {
        return this.lib_publisher_ != null && this.native_handle_ != 0;
    }

    public final boolean is_rtmp_publishing() { return is_rtmp_publishing_; }

    public final boolean is_recording() { return is_recording_; }

    public final boolean is_rtsp_publishing() { return is_rtsp_publishing_; }

    public final boolean is_gb_stream_publishing() { return is_gb_stream_publishing_; }

    public final boolean is_publishing() { return is_gb_stream_publishing_ || is_rtmp_publishing_ || is_rtsp_publishing_ || is_recording_; }

    public boolean SetMute(boolean is_mute) {
        if (!check_native_handle())
            return false;

        return OK == lib_publisher_.SmartPublisherSetMute(get(), is_mute? 1 : 0);
    }

    public boolean SetInputAudioVolume(int index, float volume) {
        if (!check_native_handle())
            return false;

        return OK == lib_publisher_.SmartPublisherSetInputAudioVolume(get(), index , volume);
    }

    public boolean SetMirror(boolean is_mirror) {
        if (!check_native_handle())
            return false;

        return OK == lib_publisher_.SmartPublisherSetMirror(get(), is_mirror?1:0);
    }

    public boolean SetRecorderDirectory(String path) {
        if (!check_native_handle())
            return false;

        return OK == lib_publisher_.SmartPublisherSetRecorderDirectory(get(), path);
    }

    public boolean SetRecorderFileMaxSize(int size) {
        if (!check_native_handle())
            return false;

        return OK == lib_publisher_.SmartPublisherSetRecorderFileMaxSize(get(), size);
    }

    public boolean CaptureImage(int compress_format, int quality, String file_name, String user_data_string) {
        if (!check_native_handle())
            return false;

        return OK == lib_publisher_.CaptureImage(get(), compress_format, quality, file_name, user_data_string);
    }

    public boolean SetURL(String url) {
        if (!check_native_handle())
            return false;

        return OK == lib_publisher_.SmartPublisherSetURL(get(), url);
    }

    public boolean StartPublisher() {
        if (!check_native_handle())
            return false;

        if (is_rtmp_publishing()) {
            Log.e(TAG, "already publishing rtmp, native_handle:" + get());
            return false;
        }

        int ret = lib_publisher_.SmartPublisherStartPublisher(get());
        if (ret != OK) {
            Log.e(TAG, "call SmartPublisherStartPublisher failed, native_handle:" + get() + ", ret:" + ret);
            return false;
        }

        write_lock_.lock();
        try {
            this.is_rtmp_publishing_ = true;
        } finally {
            write_lock_.unlock();
        }

        Log.i(TAG, "call SmartPublisherStartPublisher OK, native_handle:" + get());
        return true;
    }

    public boolean StopPublisher() {
        if (!check_native_handle())
            return false;

        if (!is_rtmp_publishing()) {
            Log.w(TAG, "it's not publishing rtmp, native_handle:" + get());
            return false;
        }

        boolean is_need_call = false;
        write_lock_.lock();
        try {
            if (this.is_rtmp_publishing_) {
                this.is_rtmp_publishing_ = false;
                is_need_call = true;
            }
        } finally {
            write_lock_.unlock();
        }

        if (is_need_call)
            lib_publisher_.SmartPublisherStopPublisher(get());

        return true;
    }

    public boolean StartRecorder() {
        if (!check_native_handle())
            return false;

        if (is_recording()) {
            Log.e(TAG, "already recording, native_handle:" + get());
            return false;
        }

        int ret = lib_publisher_.SmartPublisherStartRecorder(get());
        if (ret != OK) {
            Log.e(TAG, "call SmartPublisherStartRecorder failed, native_handle:" + get() + ", ret:" + ret);
            return false;
        }

        write_lock_.lock();
        try {
            this.is_recording_ = true;
        } finally {
            write_lock_.unlock();
        }

        Log.i(TAG, "call SmartPublisherStartRecorder OK, native_handle:" + get());
        return true;
    }

    public boolean PauseRecorder(boolean is_pause) {
        if (!check_native_handle())
            return false;

        if (!is_recording()) {
            Log.e(TAG, "it's not recording, native_handle:" + get());
            return false;
        }

        return OK == lib_publisher_.SmartPublisherPauseRecorder(get(), is_pause?1:0);
    }

    public boolean StopRecorder() {
        if (!check_native_handle())
            return false;

        if (!is_recording()) {
            Log.w(TAG, "it's not recording, native_handle:" + get());
            return false;
        }

        boolean is_need_call = false;
        write_lock_.lock();
        try {
            if (this.is_recording_) {
                this.is_recording_ = false;
                is_need_call = true;
            }
        } finally {
            write_lock_.unlock();
        }

        if (is_need_call)
            lib_publisher_.SmartPublisherStopRecorder(get());

        return true;
    }

    public boolean SetRtspStreamName(String stream_name) {
        if (is_null_or_empty(stream_name))
            return false;

        if (!check_native_handle())
            return false;

        return OK == lib_publisher_.SetRtspStreamName(get(), stream_name);
    }

    public boolean AddRtspStreamServer(long rtsp_server_handle) {
        if (!check_native_handle())
            return false;

        return OK == lib_publisher_.AddRtspStreamServer(get(), rtsp_server_handle, 0);
    }

    public boolean ClearRtspStreamServer() {
        if (!check_native_handle())
            return false;

        return OK == lib_publisher_.ClearRtspStreamServer(get());
    }

    public boolean StartRtspStream() {
        if (!check_native_handle())
            return false;

        if (is_rtsp_publishing()) {
            Log.e(TAG, "already publishing rtsp stream, native_handle:" + get());
            return false;
        }

        int ret = lib_publisher_.StartRtspStream(get(), 0);
        if (ret != OK) {
            Log.e(TAG, "call StartRtspStream failed, native_handle:" + get() + ", ret:" + ret);
            return false;
        }

        write_lock_.lock();
        try {
            this.is_rtsp_publishing_ = true;
        } finally {
            write_lock_.unlock();
        }

        Log.i(TAG, "call StartRtspStream OK, native_handle:" + get());
        return true;
    }

    public boolean StopRtspStream() {
        if (!check_native_handle())
            return false;

        if (!is_rtsp_publishing()) {
            Log.w(TAG, "it's not publishing rtsp stream, native_handle:" + get());
            return false;
        }

        boolean is_need_call = false;
        write_lock_.lock();
        try {
            if (this.is_rtsp_publishing_) {
                this.is_rtsp_publishing_ = false;
                is_need_call = true;
            }
        } finally {
            write_lock_.unlock();
        }

        if (is_need_call)
            lib_publisher_.StopRtspStream(get());

        return true;
    }

    public boolean SetGB28181RTPSender(long rtp_sender_handle, int rtp_payload_type, String encoding_name) {
        if (!check_native_handle())
            return false;

        return OK == lib_publisher_.SetGB28181RTPSender(get(), rtp_sender_handle, rtp_payload_type, encoding_name);
    }

    public boolean StartGB28181MediaStream() {
        if (!check_native_handle())
            return false;

        if (is_gb_stream_publishing()) {
            Log.e(TAG, "already publishing gb media stream, native_handle:" + get());
            return false;
        }

        int ret = lib_publisher_.StartGB28181MediaStream(get());
        if (ret != OK) {
            Log.e(TAG, "call StartGB28181MediaStream failed, native_handle:" + get() + ", ret:" + ret);
            return false;
        }

        write_lock_.lock();
        try {
            this.is_gb_stream_publishing_ = true;
        } finally {
            write_lock_.unlock();
        }

        Log.i(TAG, "call StartGB28181MediaStream OK,native_handle:" + get());

        return true;
    }

    public boolean StopGB28181MediaStream() {
        if (!check_native_handle())
            return false;

        if (!is_gb_stream_publishing()) {
            Log.w(TAG, "it's not publishing gb stream, native_handle:" + get());
            return false;
        }

        boolean is_need_call = false;
        write_lock_.lock();
        try {
            if (this.is_gb_stream_publishing_) {
                this.is_gb_stream_publishing_ = false;
                is_need_call = true;
            }
        } finally {
            write_lock_.unlock();
        }

        if (is_need_call)
            lib_publisher_.StopGB28181MediaStream(get());

        return true;
    }

    public boolean OnPCMData(ByteBuffer pcm_data, int size, int sample_rate, int channel, int per_channel_sample_number) {
        if (!check_native_handle() || !is_publishing())
            return false;

        if (!read_lock_.tryLock())
            return false;

        try {
            if (!check_native_handle() || !is_publishing())
                return false;

            return OK == lib_publisher_.SmartPublisherOnPCMData(get(), pcm_data, size, sample_rate, channel, per_channel_sample_number);
        } catch (Exception e) {
            Log.e(TAG, "OnPCMData Exception:", e);
            return false;
        } finally {
            read_lock_.unlock();
        }
    }

    public boolean OnFarEndPCMData(ByteBuffer pcm_data, int sample_rate, int channel, int per_channel_sample_number, int is_low_latency) {
        if (!check_native_handle() || !is_publishing())
            return false;

        if (!read_lock_.tryLock())
            return false;

        try {
            if (!check_native_handle() || !is_publishing())
                return false;

            return OK == lib_publisher_.SmartPublisherOnFarEndPCMData(get(), pcm_data, sample_rate, channel, per_channel_sample_number, is_low_latency);
        } catch (Exception e) {
            Log.e(TAG, "OnFarEndPCMData Exception:", e);
            return false;
        } finally {
            read_lock_.unlock();
        }
    }

    public boolean PostUserUTF8StringData(String utf8_string) {
        if (is_null_or_empty(utf8_string))
            return false;

        if (!check_native_handle() || !is_publishing())
            return false;

        if (!read_lock_.tryLock())
            return false;

        try {
            if (!check_native_handle() || !is_publishing())
                return false;

            return OK == lib_publisher_.SmartPublisherPostUserUTF8StringData(get(), utf8_string, 0);
        } catch (Exception e) {
            Log.e(TAG, "PostUserUTF8StringData Exception:", e);
            return false;
        } finally {
            read_lock_.unlock();
        }
    }

    public boolean OnCaptureVideoData(byte[] data, int len, int cameraType, int curOrg) {
        if (!check_native_handle())
            return false;

        if (!read_lock_.tryLock())
            return false;

        try {
            if (!check_native_handle())
                return false;

            return OK == lib_publisher_.SmartPublisherOnCaptureVideoData(get(), data, len, cameraType, curOrg);
        } catch (Exception e) {
            Log.e(TAG, "OnCaptureVideoData Exception:", e);
            return false;
        } finally {
            read_lock_.unlock();
        }
    }

    public boolean EnableLayer(int index, boolean is_enable) {
        if (index < 1)
            return false;

        if (!check_native_handle())
            return false;

        read_lock_.lock();

        try {
            if (!check_native_handle())
                return false;

            return OK == lib_publisher_.EnableLayer(get(), index, is_enable?1:0);
        } catch (Exception e) {
            Log.e(TAG, "EnableLayer Exception:", e);
            return false;
        } finally {
            read_lock_.unlock();
        }
    }

    public boolean RemoveLayer(int index) {
        if (index < 1)
            return false;

        if (!check_native_handle())
            return false;

        read_lock_.lock();

        try {
            if (!check_native_handle())
                return false;

            return OK == lib_publisher_.RemoveLayer(get(), index);
        } catch (Exception e) {
            Log.e(TAG, "RemoveLayer Exception:", e);
            return false;
        } finally {
            read_lock_.unlock();
        }
    }

    public boolean PostLayerImageNV21ByteArray(int index, int left, int top,
                                               byte[] y_plane, int y_offset, int y_row_stride,
                                               byte[] uv_plane, int uv_offset, int uv_row_stride,
                                               int width, int height, int is_vertical_flip,  int is_horizontal_flip,
                                               int scale_width,  int scale_height, int scale_filter_mode,
                                               int rotation_degree) {
        if (!check_native_handle())
            return false;

        if (!read_lock_.tryLock())
            return false;

        try {
            if (!check_native_handle())
                return false;

            return OK == lib_publisher_.PostLayerImageNV21ByteArray(get(), index, left, top, y_plane, y_offset, y_row_stride,
                    uv_plane, uv_offset, uv_row_stride, width, height, is_vertical_flip, is_horizontal_flip,
                    scale_width, scale_height, scale_filter_mode, rotation_degree);
        } catch (Exception e) {
            Log.e(TAG, "PostLayerImageNV21ByteArray Exception:", e);
            return false;
        } finally {
            read_lock_.unlock();
        }
    }

    public boolean PostLayerImageRGBA8888ByteBuffer(int index, int left, int top,
                                                    ByteBuffer rgba_plane, int offset, int row_stride, int width, int height,
                                                    int is_vertical_flip,  int is_horizontal_flip,
                                                    int scale_width,  int scale_height, int scale_filter_mode,
                                                    int rotation_degree) {

        if (!check_native_handle())
            return false;

        if (!read_lock_.tryLock())
            return false;

        try {
            if (!check_native_handle())
                return false;

            return OK == lib_publisher_.PostLayerImageRGBA8888ByteBuffer(get(), index, left, top,
                    rgba_plane, offset, row_stride, width, height, is_vertical_flip, is_horizontal_flip,
                    scale_width, scale_height, scale_filter_mode, rotation_degree);

        } catch (Exception e) {
            Log.e(TAG, "PostLayerImageRGBA8888ByteBuffer Exception:", e);
            return false;
        } finally {
            read_lock_.unlock();
        }
    }

    public boolean PostLayerImageI420ByteBuffer(int index, int left, int top,
                                                ByteBuffer y_plane, int y_offset, int y_row_stride,
                                                ByteBuffer u_plane, int u_offset, int u_row_stride,
                                                ByteBuffer v_plane, int v_offset, int v_row_stride,
                                                int width, int height, int is_vertical_flip,  int is_horizontal_flip,
                                                int scale_width,  int scale_height, int scale_filter_mode,
                                                int rotation_degree) {
        if (!check_native_handle())
            return false;

        if (!read_lock_.tryLock())
            return false;

        try {
            if (!check_native_handle())
                return false;

            return OK == lib_publisher_.PostLayerImageI420ByteBuffer(get(), index, left, top,
                    y_plane, y_offset, y_row_stride,
                    u_plane, u_offset, u_row_stride,
                    v_plane, v_offset, v_row_stride,
                    width, height, is_vertical_flip, is_horizontal_flip,
                    scale_width, scale_height, scale_filter_mode,
                    rotation_degree);

        } catch (Exception e) {
            Log.e(TAG, "PostLayerImageI420ByteBuffer Exception:", e);
            return false;
        } finally {
            read_lock_.unlock();
        }
    }

    public boolean PostLayerImageYUV420888ByteBuffer(int index, int left, int top,
                                                     ByteBuffer y_plane, int y_offset, int y_row_stride,
                                                     ByteBuffer u_plane, int u_offset, int u_row_stride,
                                                     ByteBuffer v_plane, int v_offset, int v_row_stride, int uv_pixel_stride,
                                                     int width, int height, int is_vertical_flip,  int is_horizontal_flip,
                                                     int scale_width,  int scale_height, int scale_filter_mode,
                                                     int rotation_degree) {
        if (!check_native_handle())
            return false;

        if (!read_lock_.tryLock())
            return false;

        try {
            if (!check_native_handle())
                return false;

            return OK == lib_publisher_.PostLayerImageYUV420888ByteBuffer(get(), index, left, top, y_plane, y_offset, y_row_stride,
             u_plane, u_offset, u_row_stride, v_plane, v_offset, v_row_stride, uv_pixel_stride,
             width, height, is_vertical_flip, is_horizontal_flip, scale_width, scale_height, scale_filter_mode, rotation_degree);

        } catch (Exception e) {
            Log.e(TAG, "PostLayerImageYUV420888ByteBuffer Exception:", e);
            return false;
        } finally {
            read_lock_.unlock();
        }
    }

    public boolean PostAudioEncodedData(int codec_id, ByteBuffer data, int size, int is_key_frame,
                                                      long timestamp,ByteBuffer parameter_info, int parameter_info_size) {
        if (!check_native_handle())
            return false;

        if (!read_lock_.tryLock())
            return false;

        try {
            if (!check_native_handle())
                return false;

            return OK == lib_publisher_.SmartPublisherPostAudioEncodedData(get(), codec_id, data, size, is_key_frame, timestamp, parameter_info, parameter_info_size);

        } catch (Exception e) {
            Log.e(TAG, "PostLayerImageYUV420888ByteBuffer Exception:", e);
            return false;
        } finally {
            read_lock_.unlock();
        }
    }

    private static boolean is_null_or_empty(String val) {
        return null == val || val.isEmpty();
    }

    //估计硬编码码率, 可以根据实际机型调整
    public static int estimate_video_hardware_kbps(int width, int height, int fps, boolean is_h264) {
        int kbps;
        int area = width * height;
        if (area <= (320 * 300))
            kbps = is_h264?350:280;
        else if (area <= (370 * 320))
            kbps = is_h264?470:400;
         else if (area <= (640 * 360))
            kbps = is_h264?850:650;
       else if (area <= (640 * 480))
            kbps = is_h264?1200:800;
        else if (area <= (800 * 600))
            kbps = is_h264?1300:950;
        else if (area <= (900 * 700))
            kbps = is_h264?1600:1100;
        else if (area <= (1280 * 720))
            kbps = is_h264?2100:1500;
         else if (area <= (1366 * 768))
            kbps = is_h264?2300:1900;
         else if (area <= (1600 * 900))
            kbps = is_h264?2800:2300;
        else if (area <= (1600 * 1050))
            kbps =is_h264?3100:2500;
         else if (area <= (1920 * 1088))
            kbps = is_h264?4200:2800;
        else
            kbps = is_h264?4500:3500;

        kbps = (int)(kbps*fps*1.0/25.0 + 0.5);
        return kbps;
    }

    public static int estimate_video_software_quality(int width, int height, boolean is_h264) {
        int area = width*height;
        int quality;
        if ( area <= (320 * 240) )
            quality = is_h264? 23 : 27;
        else if ( area <= (640 * 360) )
            quality = is_h264? 25 : 28;
        else if ( area <= (640 * 480) )
            quality = is_h264? 26 : 28;
        else if ( area <= (960 * 600) )
            quality = is_h264? 26 : 28;
        else if ( area <= (1280 * 720) )
            quality = is_h264? 27 : 29;
        else if ( area <= (1600 * 900) )
            quality = is_h264 ? 28 : 30;
        else if ( area <= (1920 * 1088) )
            quality = is_h264 ? 29 : 31;
        else
            quality = is_h264 ? 30 : 32;

        return quality;
    }

    public static int estimate_video_vbr_max_kbps(int width, int height, int fps) {
        int kbps;
        int area = width*height;
        if (area <= (320 * 300))
            kbps = 320;
        else if (area <= (360 * 320))
            kbps = 400;
        else if (area <= (640 * 360))
            kbps = 700;
        else if (area <= (640 * 480))
            kbps = 800;
        else if (area <= (800 * 600))
            kbps = 900;
        else if (area <= (900 * 700))
            kbps = 1100;
        else if (area <= (1280 * 720))
            kbps = 1700;
        else if (area <= (1366 * 768))
            kbps = 1800;
        else if (area <= (1600 * 900))
            kbps = 2500;
        else if (area <= (1600 * 1050))
            kbps = 2700;
        else if (area <= (1920 * 1088))
            kbps = 3100;
        else
            kbps = 3500;

        kbps = (int)(kbps*fps*1.0/25.0 + 0.5);

        return kbps;
    }
}

封装后的LibPublisherWrapper类,逻辑分离更彻底,调用更方便,几乎不要花心思了解接口用法,就可以非常高效的实现RTMP推送或轻量级RTSP服务技术诉求。

相关推荐
加油吧x青年5 小时前
Web端开启直播技术方案分享
前端·webrtc·直播
aqi0012 天前
FFmpeg开发笔记(五十二)移动端的国产视频播放器GSYVideoPlayer
android·ffmpeg·音视频·直播·流媒体
aqi0013 天前
FFmpeg开发笔记(五十一)适合学习研究的几个音视频开源框架
ffmpeg·音视频·直播·流媒体
aqi0019 天前
FFmpeg开发笔记(五十)聊聊几种流媒体传输技术的前世今生
ffmpeg·音视频·直播·流媒体
aqi0021 天前
FFmpeg开发笔记(四十九)助您在毕业设计中脱颖而出的几个流行APP
ffmpeg·音视频·直播·流媒体
aqi001 个月前
FFmpeg开发笔记(四十八)从0开始搭建直播系统的开源软件架构
ffmpeg·音视频·直播·流媒体
aqi001 个月前
FFmpeg开发笔记(四十七)寒冬下安卓程序员的几个技术转型发展方向
ffmpeg·音视频·直播·流媒体
aqi001 个月前
FFmpeg开发笔记(四十六)利用SRT协议构建手机APP的直播Demo
ffmpeg·音视频·直播·流媒体
x007xyz1 个月前
前端纯手工绘制音频波形图
前端·音视频开发·canvas
aqi001 个月前
FFmpeg开发笔记(四十五)使用SRT Streamer开启APP直播推流
ffmpeg·音视频·直播·流媒体