深度解析:基于SmartPlayer接口快速构建低延迟的RTSP|RTMP播放功能

在移动互联网时代,实时视频播放和录像功能在众多应用场景中扮演着关键角色,如视频监控、直播、远程教学等。本文将结合大牛直播SDK的Android平台RTSP/RTMP直播播放器技术接入说明,深入探讨如何实现低延迟(150-300ms)的RTSP/RTMP播放和录像功能。

一、技术背景

大牛直播SDK自2015年发布RTSP、RTMP直播播放模块以来,凭借其功能强大、性能强劲、高稳定、超低延迟和超低资源占用等优势,获得了行业内的一致认可。其SmartPlayer功能模块是跨平台RTSP、RTMP直播播放器的佼佼者。本文将以Android平台为例,详细介绍如何基于大牛直播SDK的SmartPlayer模块,实现高稳定、低延迟、功能完善性能强大的播放器。

二、技术对接

1. 系统要求

  • SDK支持Android 5.1及以上版本

  • 支持的CPU架构:armv7, arm64, x86, x86_64。

2. 准备工作

  • 确保SmartPlayerJniV2.java放到com.daniulive.smartplayer包名下(可在其他包名下调用);

  • Smartavengine.jar加入到工程

  • 拷贝libSmartPlayer.so到工程

    • SmartPlayerV2\app\src\main\jniLibs\armeabi-v7a

    • SmartPlayerV2\app\src\main\jniLibs\arm64-v8a

    • SmartPlayerV2\app\src\main\jniLibs\x86

    • SmartPlayerV2\app\src\main\jniLibs\x86_64

  • AndroidManifest.xml添加相关权限

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
  • Load相关so

    java复制

    static {
        System.loadLibrary("SmartPlayer");
    }
  • build.gradle配置32/64位库

    gradle复制

    splits {
        abi {
            enable true
            include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
            universalApk true
        }
    }
  • 如需集成到自己系统测试,请用大牛直播SDK的app name,授权版按照授权app name正常使用即可

  • 如何改app-name,strings.xml做以下修改

    <string name="app_name">SmartPlayerSDKDemo</string>

三、接口设计

1. 调用描述

调用描述接口接口描述
最先调用,如成功返回播放实例SmartPlayerOpenplayer初始化,设置上下文信息,返回player句柄
Event回调SetSmartPlayerEventCallbackV2设置event callback
硬解码设置(H.264)SetSmartPlayerVideoHWDecoder设置是否用H.264硬解码播放,如硬解码不支持,自动适配到软解码
硬解码设置(H.265)SetSmartPlayerVideoHevcHWDecoder设置是否用H.265硬解码播放,如硬解码不支持,自动适配到软解码
视频画面填充模式SmartPlayerSetRenderScaleMode设置视频画面的填充模式,如填充整个view、等比例填充view,如不设置,默认填充整个view
设置SurfaceView模式下render类型SmartPlayerSetSurfaceRenderFormat设置SurfaceView模式下(NTRenderer.CreateRenderer第二个参数传false的情况),render类型
设置SurfaceView模式下抗锯齿效果SmartPlayerSetSurfaceAntiAlias设置SurfaceView模式下(NTRenderer.CreateRenderer第二个参数传false的情况),抗锯齿效果,注意:抗锯齿模式开启后,可能会影像性能,请慎用
设置播放的surfaceSmartPlayerSetSurface设置播放的surface,如果为null,则播放纯音频
设置视频硬解码下Mediacodec自行绘制模式SmartPlayerSetHWRenderMode此种模式下,硬解码兼容性和效率更好,回调YUV/RGB、快照和图像等比例缩放功能将不可用
更新硬解码surfaceSmartPlayerUpdateHWRenderSurface设置更新硬解码surface
音频回调SmartPlayerSetExternalAudioOutput回调audio数据到上层(供二次处理之用)
audio输出类型SmartPlayerSetAudioOutputType如果use_audiotrack设置为0,将会自动选择输出设备,如果设置为1,使用audiotrack模式,一对一回音消除模式下,请选用audiotrack模式
播放模式SmartPlayerSetBuffer设置播放端缓存数据buffer,单位:毫秒,如不需buffer,设置为0
首屏秒开SmartPlayerSetFastStartup设置快速启动后,如果CDN缓存GOP,实现首屏秒开
低延迟模式SmartPlayerSetLowLatencyMode针对类似于直播娃娃机等期待超低延迟的使用场景,超低延迟播放模式下,延迟可达到200~400ms
快速切换URLSmartPlayerSwitchPlaybackUrl快速切换播放url,快速切换时,只换播放source部分,适用于不同数据流之间,快速切换(如娃娃机双摄像头切换或高低分辨率流切换)
RTSP TCP/UDP模式设置SmartPlayerSetRTSPTcpMode设置RTSP TCP/UDP模式,如不设置,默认UDP模式
RTSP超时时间设置SmartPlayerSetRTSPTimeout设置RTSP超时时间,timeout单位为秒,必须大于0
设置RTSP TCP/UDP自动切换SmartPlayerSetRTSPAutoSwitchTcpUdp对于RTSP来说,有些可能支持rtp over udp方式,有些可能支持使用rtp over tcp方式,为了方便使用,有些场景下可以开启自动尝试切换开关,打开后如果udp无法播放,sdk会自动尝试tcp,如果tcp方式播放不了,sdk会自动尝试udp
设置RTSP用户名和密码SetRTSPAuthenticationInfo如果RTSP URL已包含用户名和密码,此接口设置的用户名和密码将无效。就是要用这个接口设置的用户名和密码去做认证,RTSP URL不能包含用户名和密码
实时静音SmartPlayerSetMute实时静音
设置播放音量SmartPlayerSetAudioVolume播放端音量实时调节,范围[0,100],0时为静音,100为原始流数据最大音量
设置是否禁用Enhanced RTMPDisableEnhancedRTMPdisable enhanced RTMP,SDK默认是开启enhanced RTMP的
实时截图CaptureImage支持JPEG和PNG两种格式
视频镜像旋转SmartPlayerSetRotation设置顺时针旋转,注意除了0度之外,其他角度都会额外消耗性能,当前支持0度,90度, 180度, 270度旋转
水平反转SmartPlayerSetFlipHorizontal设置视频水平反转
垂直反转SmartPlayerSetFlipVertical设置视频垂直反转
设置URLSmartPlayerSetUrl设置需要播放或录像的RTMP/RTSP url
开始播放SmartPlayerStartPlay开始播放RTSP/RTMP流
停止播放SmartPlayerStopPlay停止播放RTSP/RTMP流
关闭播放实例SmartPlayerClose结束时必须调用close接口释放资源

四、功能支持

  • 音频:AAC/Speex(RTMP)/PCMA/PCMU;

  • 视频:H.264、H.265;

  • 播放协议:RTSP/RTMP;

  • 支持纯音频、纯视频、音视频播放;

  • 支持多实例播放;

  • 支持软解码,特定机型硬解码;

  • 支持RTSP TCP、UDP模式设置;

  • 支持RTSP TCP、UDP模式自动切换;

  • 支持RTSP超时时间设置,单位:秒;

  • 支持buffer时间设置,单位:毫秒;

  • 支持超低延迟模式;

  • 支持断网自动重连、视频追赶,支持buffer状态等回调;

  • 支持视频view实时旋转(0° 90° 180° 270°);

  • 支持视频view水平反转、垂直反转;

  • 支持Surfaceview/OpenGL ES/TextureView绘制;

  • 支持视频画面填充模式设置;

  • 音频支持AudioTrack、OpenSL ES模式;

  • 支持jpeg、png实时截图;

  • 支持实时音量调节;

  • 支持解码前音视频数据回调;

  • 支持解码后YUV/RGB数据回调;

  • 支持Enhanced RTMP;

  • 支持扩展录像功能;

  • 支持Android 5.1及以上版本。

五、接口调用详解

1. 初始化播放器

onCreate()方法中,首先创建SmartPlayerJniV2实例:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_smart_player);
    libPlayer = new SmartPlayerJniV2();
    myContext = this.getApplicationContext();
}

2. 开始播放和停止播放

实现开始播放和停止播放的逻辑,开始播放时调用InitAndSetConfig()完成常规参数初始化,然后调用仅播放相关的其他接口:

java复制

btnStartStopPlayback.setOnClickListener(new Button.OnClickListener() {
    public void onClick(View v) {
        if (isPlaying) {
            Log.i(TAG, "Stop playback stream++");
            int iRet = libPlayer.SmartPlayerStopPlay(playerHandle);
            if (iRet != 0) {
                Log.e(TAG, "Call SmartPlayerStopPlay failed..");
                return;
            }
            btnHardwareDecoder.setEnabled(true);
            btnLowLatency.setEnabled(true);
            if (!isRecording) {
                btnPopInputUrl.setEnabled(true);
                btnSetPlayBuffer.setEnabled(true);
                btnFastStartup.setEnabled(true);
                btnRecoderMgr.setEnabled(true);
                libPlayer.SmartPlayerClose(playerHandle);
                playerHandle = 0;
            }
            isPlaying = false;
            btnStartStopPlayback.setText("开始播放 ");
            if (is_enable_hardware_render_mode && sSurfaceView != null) {
                sSurfaceView.setVisibility(View.GONE);
                sSurfaceView.setVisibility(View.VISIBLE);
            }
            Log.i(TAG, "Stop playback stream--");
        } else {
            Log.i(TAG, "Start playback stream++");
            if (!isRecording) {
                InitAndSetConfig();
            }
            libPlayer.SmartPlayerSetSurface(playerHandle, sSurfaceView);
            libPlayer.SmartPlayerSetRenderScaleMode(playerHandle, 1);
            if (isHardwareDecoder && is_enable_hardware_render_mode) {
                libPlayer.SmartPlayerSetHWRenderMode(playerHandle, 1);
            }
            libPlayer.SmartPlayerSetUserDataCallback(playerHandle, new UserDataCallback());
            libPlayer.SmartPlayerSetAudioOutputType(playerHandle, 1);
            if (isMute) {
                libPlayer.SmartPlayerSetMute(playerHandle, isMute ? 1 : 0);
            }
            if (isHardwareDecoder) {
                int isSupportHevcHwDecoder = libPlayer.SetSmartPlayerVideoHevcHWDecoder(playerHandle, 1);
                int isSupportH264HwDecoder = libPlayer.SetSmartPlayerVideoHWDecoder(playerHandle, 1);
                Log.i(TAG, "isSupportH264HwDecoder: " + isSupportH264HwDecoder + ", isSupportHevcHwDecoder: " + isSupportHevcHwDecoder);
            }
            libPlayer.SmartPlayerSetLowLatencyMode(playerHandle, isLowLatency ? 1 : 0);
            libPlayer.SmartPlayerSetFlipVertical(playerHandle, is_flip_vertical ? 1 : 0);
            libPlayer.SmartPlayerSetFlipHorizontal(playerHandle, is_flip_horizontal ? 1 : 0);
            libPlayer.SmartPlayerSetRotation(playerHandle, rotate_degrees);
            libPlayer.SmartPlayerSetAudioVolume(playerHandle, curAudioVolume);
            int iPlaybackRet = libPlayer.SmartPlayerStartPlay(playerHandle);
            if (iPlaybackRet != 0) {
                Log.e(TAG, "Call SmartPlayerStartPlay failed..");
                return;
            }
            btnStartStopPlayback.setText("停止播放 ");
            btnPopInputUrl.setEnabled(false);
            btnPopInputKey.setEnabled(false);
            btnSetPlayBuffer.setEnabled(false);
            btnLowLatency.setEnabled(false);
            btnFastStartup.setEnabled(false);
            btnRecoderMgr.setEnabled(false);
            isPlaying = true;
            Log.i(TAG, "Start playback stream--");
        }
    }
});

3. 初始化配置

InitAndSetConfig()方法用于初始化播放器配置:

private void InitAndSetConfig() {
    playerHandle = libPlayer.SmartPlayerOpen(myContext);
    if (playerHandle == 0) {
        Log.e(TAG, "surfaceHandle with nil..");
        return;
    }
    libPlayer.SetSmartPlayerEventCallbackV2(playerHandle, new EventHandeV2());
    libPlayer.SmartPlayerSetBuffer(playerHandle, playBuffer);
    libPlayer.SmartPlayerSetReportDownloadSpeed(playerHandle, 1, 2);
    libPlayer.SmartPlayerSetFastStartup(playerHandle, isFastStartup ? 1 : 0);
    int rtsp_timeout = 10;
    libPlayer.SmartPlayerSetRTSPTimeout(playerHandle, rtsp_timeout);
    int is_auto_switch_tcp_udp = 1;
    libPlayer.SmartPlayerSetRTSPAutoSwitchTcpUdp(playerHandle, is_auto_switch_tcp_udp);
    libPlayer.SmartPlayerSaveImageFlag(playerHandle, 1);
    if (playbackUrl == null) {
        Log.e(TAG, "playback URL with NULL...");
        return;
    }
    libPlayer.SmartPlayerSetUrl(playerHandle, playbackUrl);
}

4. 事件回调处理

事件回调处理是底层状态反馈的重要媒介,除了网络状态、buffering状态回调外,还有录像状态、快照状态等回调:

class EventHandeV2 implements NTSmartEventCallbackV2 {
    @Override
    public void onNTSmartEventCallbackV2(long handle, int id, long param1, long param2, String param3, String param4, Object param5) {
        String player_event = "";
        switch (id) {
            case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_STARTED:
                player_event = "开始..";
                break;
            case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTING:
                player_event = "连接中..";
                break;
            case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTION_FAILED:
                player_event = "连接失败..";
                break;
            case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTED:
                player_event = "连接成功..";
                break;
            case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_DISCONNECTED:
                player_event = "连接断开..";
                break;
            case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_STOP:
                player_event = "停止播放..";
                break;
            case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_RESOLUTION_INFO:
                player_event = "分辨率信息: width: " + param1 + ", height: " + param2;
                break;
            case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_NO_MEDIADATA_RECEIVED:
                player_event = "收不到媒体数据,可能是url错误..";
                break;
            case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_SWITCH_URL:
                player_event = "切换播放URL..";
                break;
            case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CAPTURE_IMAGE:
                player_event = "快照: " + param1 + " 路径:" + param3;
                if (param1 == 0)
                    player_event = player_event + ", 截取快照成功";
                else
                    player_event = player_event + ", 截取快照失败";
                if (param4 != null && !param4.isEmpty())
                    player_event += (", user data:" + param4);
                break;
            case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_RECORDER_START_NEW_FILE:
                player_event = "[record]开始一个新的录像文件 : " + param3;
                break;
            case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_ONE_RECORDER_FILE_FINISHED:
                player_event = "[record]已生成一个录像文件 : " + param3;
                break;
            case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_START_BUFFERING:
                Log.i(TAG, "Start Buffering");
                break;
            case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_BUFFERING:
                Log.i(TAG, "Buffering:" + param1 + "%");
                break;
            case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_STOP_BUFFERING:
                Log.i(TAG, "Stop Buffering");
                break;
            case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_DOWNLOAD_SPEED:
                player_event = "download_speed:" + param1 + "Byte/s" + ", " + (param1 * 8 / 1000) + "kbps" + ", " + (param1 / 1024) + "KB/s";
                break;
            case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_RTSP_STATUS_CODE:
                Log.e(TAG, "RTSP error code received, please make sure username/password is correct, error code:" + param1);
                player_event = "RTSP error code:" + param1;
                break;
        }
        if (player_event.length() > 0) {
            Log.i(TAG, player_event);
            Message message = new Message();
            message.what = PLAYER_EVENT_MSG;
            message.obj = player_event;
            handler.sendMessage(message);
        }
    }
}

5. 录像功能

如果需要对RTSP/RTMP流进行录像,可以实现如下逻辑:

btnStartStopRecorder.setOnClickListener(new Button.OnClickListener() {
    public void onClick(View v) {
        if (isRecording) {
            int iRet = libPlayer.SmartPlayerStopRecorder(playerHandle);
            if (iRet != 0) {
                Log.e(TAG, "Call SmartPlayerStopRecorder failed..");
                return;
            }
            if (!isPlaying) {
                btnPopInputUrl.setEnabled(true);
                btnSetPlayBuffer.setEnabled(true);
                btnFastStartup.setEnabled(true);
                btnRecoderMgr.setEnabled(true);
                libPlayer.SmartPlayerClose(playerHandle);
                playerHandle = 0;
            }
            btnStartStopRecorder.setText(" 开始录像");
            isRecording = false;
        } else {
            Log.i(TAG, "onClick start recorder..");
            if (!isPlaying) {
                InitAndSetConfig();
            }
            ConfigRecorderFunction();
            int startRet = libPlayer.SmartPlayerStartRecorder(playerHandle);
            if (startRet != 0) {
                Log.e(TAG, "Failed to start recorder.");
                return;
            }
            btnPopInputUrl.setEnabled(false);
            btnSetPlayBuffer.setEnabled(false);
            btnFastStartup.setEnabled(false);
            btnRecoderMgr.setEnabled(false);
            isRecording = true;
            btnStartStopRecorder.setText("停止录像");
        }
    }
});

6. 录像参数配置

录像参数配置选项设置如下,除了下面演示接口外,还可以设置仅录视频或音频:

void ConfigRecorderFunction() {
    if (libPlayer != null) {
        int is_rec_trans_code = 1;
        libPlayer.SmartPlayerSetRecorderAudioTranscodeAAC(playerHandle, is_rec_trans_code);
        if (recDir != null && !recDir.isEmpty()) {
            int ret = libPlayer.SmartPlayerCreateFileDirectory(recDir);
            if (0 == ret) {
                if (0 != libPlayer.SmartPlayerSetRecorderDirectory(playerHandle, recDir)) {
                    Log.e(TAG, "Set recoder dir failed , path:" + recDir);
                    return;
                }
                if (0 != libPlayer.SmartPlayerSetRecorderFileMaxSize(playerHandle, 200)) {
                    Log.e(TAG, "SmartPublisherSetRecorderFileMaxSize failed.");
                    return;
                }
            } else {
                Log.e(TAG, "Create recorder dir failed, path:" + recDir);
            }
        }
    }
}

7. 实时截图

如果需要在播放过程中实时截图,可以实现如下逻辑:

btnCaptureImage.setOnClickListener(new Button.OnClickListener() {
    @SuppressLint("SimpleDateFormat")
    public void onClick(View v) {
        if (0 == playerHandle)
            return;
        if (null == capture_image_date_format_)
            capture_image_date_format_ = new SimpleDateFormat("yyyyMMdd_HHmmss_SSS");
        String timestamp = capture_image_date_format_.format(new Date());
        String imageFileName = timestamp;
        String image_path = imageSavePath + "/" + imageFileName;
        int quality;
        boolean is_jpeg = true;
        if (is_jpeg) {
            image_path += ".jpeg";
            quality = 100;
        } else {
            image_path += ".png";
            quality = 100;
        }
        int capture_ret = libPlayer.CaptureImage(playerHandle, is_jpeg ? 0 : 1, quality, image_path, "test cix");
        Log.i(TAG, "capture image ret:" + capture_ret + ", file:" + image_path);
    }
});

8. 视频翻转和旋转

如果需要对视频进行水平、垂直翻转或旋转,可以实现如下逻辑:

btnFlipVertical.setOnClickListener(new Button.OnClickListener() {
    public void onClick(View v) {
        is_flip_vertical = !is_flip_vertical;
        if (is_flip_vertical) {
            btnFlipVertical.setText("取消反转");
        } else {
            btnFlipVertical.setText("垂直反转");
        }
        if (playerHandle != 0) {
            libPlayer.SmartPlayerSetFlipVertical(playerHandle, is_flip_vertical ? 1 : 0);
        }
    }
});
btnFlipHorizontal.setOnClickListener(new Button.OnClickListener() {
    public void onClick(View v) {
        is_flip_horizontal = !is_flip_horizontal;
        if (is_flip_horizontal) {
            btnFlipHorizontal.setText("取消反转");
        } else {
            btnFlipHorizontal.setText("水平反转");
        }
        if (playerHandle != 0) {
            libPlayer.SmartPlayerSetFlipHorizontal(playerHandle, is_flip_horizontal ? 1 : 0);
        }
    }
});
btnRotation.setOnClickListener(new Button.OnClickListener() {
    public void onClick(View v) {
        rotate_degrees += 90;
        rotate_degrees = rotate_degrees % 360;
        if (0 == rotate_degrees) {
            btnRotation.setText("旋转90度");
        } else if (90 == rotate_degrees) {
            btnRotation.setText("旋转180度");
        } else if (180 == rotate_degrees) {
            btnRotation.setText("旋转270度");
        } else if (270 == rotate_degrees) {
            btnRotation.setText("不旋转");
        }
        if (playerHandle != 0) {
            libPlayer.SmartPlayerSetRotation(playerHandle, rotate_degrees);
        }
    }
});

9. 释放资源

onDestroy()方法中,停止播放、录像,并释放播放器实例:

@Override
protected void onDestroy() {
    Log.i(TAG, "Run into activity destory++");
    if (playerHandle != 0) {
        if (isPlaying) {
            libPlayer.SmartPlayerStopPlay(playerHandle);
        }
        if (isRecording) {
            libPlayer.SmartPlayerStopRecorder(playerHandle);
        }
        libPlayer.SmartPlayerClose(playerHandle);
        playerHandle = 0;
    }
    super.onDestroy();
    finish();
    System.exit(0);
}

六、总结

通过上述步骤,我们成功实现了基于大牛直播SDK的Android平台低延迟RTSP/RTMP播放和录像功能。这些功能在视频监控、直播、远程教学等场景中具有广泛的应用价值。希望本文的详细介绍和代码示例能够帮助开发者更好地理解和应用这些技术。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值