庖丁解牛之-Android平台RTSP|RTMP播放器设计

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 我们在做Android平台RTSP或者RTMP播放器开发的时候,需要注意的点非常多,以下,以大牛直播SDK(官方)的接口为例,大概介绍下相关接口设计:

背景

我们在做Android平台RTSP或者RTMP播放器开发的时候,需要注意的点非常多,以下,以大牛直播SDK(官方)的接口为例,大概介绍下相关接口设计:

接口设计

1. Open() 接口

Open接口的目的,主要是创建实例,正常返回player实例句柄,如有多路播放诉求,创建多个实例即可。

  /**
   * Initialize Player(启动播放实例)
   *
   * @param ctx: get by this.getApplicationContext()
   *
   * <pre>This function must be called firstly.</pre>
   *
   * @return player handle if successful, if return 0, which means init failed. 
   */
  public native long SmartPlayerOpen(Object ctx);

2. Close()接口

Close接口,和Open()接口对应,负责释放相应实例的资源,调用Close()接口后,记得实例句柄置0即可。


注意:比如一个实例既可以实现播放,又可同时录像,亦或拉流(转发),这种情况下,调Close()接口时,需要确保录像、拉流都正常停止后,再调用。

  /**
   * 关闭播放实例,结束时必须调用close接口释放资源
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * <pre> NOTE: it could not use player handle after call this function. </pre> 
   *
   * @return {0} if successful
   */
  public native int SmartPlayerClose(long handle);

3. 网络状态回调

一个好的播放器,好的状态回调必不可少,比如网络连通状态、快照、录像状态、当前下载速度等实时反馈,可以让上层开发者更好的掌控播放端状态,给用户更好的播放体验。

  /**
   * Set callback event(设置事件回调)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param callbackv2: callback function
   *
   * @return {0} if successful
   */
  public native int SetSmartPlayerEventCallbackV2(long handle, NTSmartEventCallbackV2 callbackv2);

demo实现实例:

    class EventHandeV2 implements NTSmartEventCallbackV2 {
        @Override
        public void onNTSmartEventCallbackV2(long handle, int id, long param1,
                                             long param2, String param3, String param4, Object param5) {
            //Log.i(TAG, "EventHandeV2: handle=" + handle + " id:" + id);
            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 + ", 截取快照失败";
                    }
                    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;
                case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_NEED_KEY:
                    Log.e(TAG, "RTMP加密流,请设置播放需要的Key..");
                    player_event = "RTMP加密流,请设置播放需要的Key..";
                    break;
                case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_KEY_ERROR:
                    Log.e(TAG, "RTMP加密流,Key错误,请重新设置..");
                    player_event = "RTMP加密流,Key错误,请重新设置..";
                    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);
            }
        }
    }

4. 软解码还是硬解码?

随着Android发展越来越好,各个厂商芯片对硬解码的支持,也越来越友好,一般情况下,如果适合通用的产品,在设备性能保障的情况下,优先建议软解,如果特定机型设备,可酌情考虑硬解,硬解码,又分为264硬解、HEVC硬解,直接设置surface模式的硬解,接口如下:

  /**
   * Set Video H.264 HW decoder(设置H.264硬解码)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param isHWDecoder: 0: software decoder; 1: hardware decoder.
   *
   * @return {0} if successful
   */
  public native int SetSmartPlayerVideoHWDecoder(long handle, int isHWDecoder);
  /**
   * Set Video H.265(hevc) HW decoder(设置H.265硬解码)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param isHevcHWDecoder: 0: software decoder; 1: hardware decoder.
   *
   * @return {0} if successful
   */
  public native int SetSmartPlayerVideoHevcHWDecoder(long handle, int isHevcHWDecoder);

设置surface模式的硬解:

  /**
   * 设置视频硬解码下Mediacodec自行绘制模式(此种模式下,硬解码兼容性和效率更好,回调YUV/RGB和快照功能将不可用)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param isHWRenderMode: 0: not enable; 1: 用SmartPlayerSetSurface设置的surface自行绘制
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetHWRenderMode(long handle, int isHWRenderMode);
  /**
   * 更新硬解码surface
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @return {0} if successful
   */
  public native int SmartPlayerUpdateHWRenderSurface(long handle);

5. 音频输出类型

音频输出,android平台支持audiotrack模式和opensl es模式,一般来说,考虑到设备通用性,建议采用audiotrack模式。

  /**
   * Set AudioOutput Type(设置audio输出类型)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param use_audiotrack:
   *
   * <pre> NOTE: if use_audiotrack with 0: it will use auto-select output devices; if with 1: will use audio-track mode. </pre>
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetAudioOutputType(long handle, int use_audiotrack);


6. 缓冲时间设置

缓冲时间,顾名思义,缓存多少数据才开始播放,比如设置2000ms的buffer time,直播模式下,收到2秒数据后,才正常播放。


加大buffer time,会增大播放延迟,好处是,网络抖动的时候,流畅性更好。

  /**
   * Set buffer(设置缓冲时间,单位:毫秒)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param buffer:
   *
   * <pre> NOTE: Unit is millisecond, range is 0-5000 ms </pre>
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetBuffer(long handle, int buffer);

7. 实时静音、实时音量调节

实时静音、实时音量调节顾名思义,播放端可以实时调整播放音量,或者直接静音掉,特别是多路播放场景下,非常有必要。


此外我们还提供了音量放大的功能,支持放大至原始音量的2倍。

  /**
   * Set mute or not(设置实时静音)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param is_mute: if with 1:mute, if with 0: does not mute
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetMute(long handle, int is_mute);
  /**
   * 设置播放音量
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param volume: 范围是[0, 100], 0是静音,100是最大音量, 默认是100
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetAudioVolume(long handle, int volume);

8. RTSP TCP-UDP模式设置、超时时间设置或模式切换

有的RTSP服务器或摄像机,只支持RTSP TCP模式或者UDP模式,这个时候,默认设置TCP、UDP模式就至关重要,此外,我们还设计支持如TCP或UDP模式收不到数据,在超时时间后,可以自动切换到UDP或TCP。

  /**
   * 设置RTSP TCP/UDP模式(默认UDP模式)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param is_using_tcp: if with 1, it will via TCP mode, while 0 with UDP mode
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetRTSPTcpMode(long handle, int is_using_tcp);
  /**
   * 设置RTSP超时时间, timeout单位为秒,必须大于0
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param timeout: RTSP timeout setting
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetRTSPTimeout(long handle, int timeout);
  /**
   * 设置RTSP TCP/UDP自动切换
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * NOTE: 对于RTSP来说,有些可能支持rtp over udp方式,有些可能支持使用rtp over tcp方式.
   * 为了方便使用,有些场景下可以开启自动尝试切换开关, 打开后如果udp无法播放,sdk会自动尝试tcp, 如果tcp方式播放不了,sdk会自动尝试udp.
   *
   * @param is_auto_switch_tcp_udp 如果设置1的话, sdk将在tcp和udp之间尝试切换播放,如果设置为0,则不尝试切换.
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetRTSPAutoSwitchTcpUdp(long handle, int is_auto_switch_tcp_udp);

9. 快速启动

快速启动,主要是针对服务器缓存GOP的场景下,快速刷到最新的数据,确保画面的持续性。

  /**
   * Set fast startup(设置快速启动模式,此模式针对服务器缓存GOP的场景有效)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param is_fast_startup: if with 1, it will second play back, if with 0: does not it
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetFastStartup(long handle, int is_fast_startup);

10. 低延迟模式

低延迟模式下,设置buffer time为0,延迟更低,适用于比如需要操控控制的超低延迟场景下。

  /**
   * Set low latency mode(设置低延迟模式)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param mode: if with 1, low latency mode, if with 0: normal mode
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetLowLatencyMode(long handle, int mode);

11. 视频view旋转、水平|垂直翻转

接口主要用于,比如原始的视频倒置等场景下,设备端无法调整时,通过播放端完成图像的正常角度播放。

  /**
   * 设置视频垂直反转
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param is_flip: 0: 不反转, 1: 反转
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetFlipVertical(long handle, int is_flip);
  /**
   * 设置视频水平反转
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param is_flip: 0: 不反转, 1: 反转
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetFlipHorizontal(long handle, int is_flip);
  /**
   * 设置顺时针旋转, 注意除了0度之外, 其他角度都会额外消耗性能
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param degress: 当前支持 0度,90度, 180度, 270度 旋转
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetRotation(long handle, int degress);

12. 设置实时回调下载速度

调用实时下载速度接口,通过设置下载速度时间间隔,和是否需要上报当前下载速度,实现APP层和底层SDK更友好的交互。

  /**
   * Set report download speed(设置实时回调下载速度)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param is_report: if with 1, it will report download speed, it with 0: does not it.
   *
   * @param report_interval: report interval, unit is second, it must be greater than 0.
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetReportDownloadSpeed(long handle, int is_report, int report_interval );

13. 实时快照

简单来说,播放过程中,是不是要存取当前的播放画面。

  /**
   * Set if needs to save image during playback stream(是否启动快照功能)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param is_save_image: if with 1, it will save current image via the interface of SmartPlayerSaveCurImage(), if with 0: does not it
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSaveImageFlag(long handle, int is_save_image);
  /**
   * Save current image during playback stream(实时快照)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param imageName: image name, which including fully path, "/sdcard/daniuliveimage/daniu.png", etc.
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSaveCurImage(long handle, String imageName);

14. 扩展录像操作

播放端录像,我们做的非常细化,比如可以只录制音频或者只录制视频,设置录像存储路径,设置单个文件size,如果非AAC数据,可以转AAC后再录像。

  /**
   * Create file directory(创建录像目录)
   *
   * @param path,  E.g: /sdcard/daniulive/rec
   *
   * <pre> The interface is only used for recording the stream data to local side. </pre>
   *
   * @return {0} if successful
   */
  public native int SmartPlayerCreateFileDirectory(String path);
  /**
   * Set recorder directory(设置录像目录)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param path: the directory of recorder file
   *
   * <pre> NOTE: make sure the path should be existed, or else the setting failed. </pre>
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetRecorderDirectory(long handle, String path);
  /**
   * Set the size of every recorded file(设置单个录像文件大小,如超过设定大小则自动切换到下个文件录制)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param size: (MB), (5M~500M), if not in this range, set default size with 200MB.
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetRecorderFileMaxSize(long handle, int size);
  /*
   * 设置录像时音频转AAC编码的开关
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * aac比较通用,sdk增加其他音频编码(比如speex, pcmu, pcma等)转aac的功能.
   *
   * @param is_transcode: 设置为1的话,如果音频编码不是aac,则转成aac,如果是aac,则不做转换. 设置为0的话,则不做任何转换. 默认是0.
   *
   * 注意: 转码会增加性能消耗
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetRecorderAudioTranscodeAAC(long handle, int is_transcode);
  /*
  *设置是否录视频,默认的话,如果视频源有视频就录,没有就没得录, 但有些场景下可能不想录制视频,只想录音频,所以增加个开关
  *
  *@param is_record_video: 1 表示录制视频, 0 表示不录制视频, 默认是1
  *
  * @return {0} if successful
  */
  public native int SmartPlayerSetRecorderVideo(long handle, int is_record_video);
  /*
  *设置是否录音频,默认的话,如果视频源有音频就录,没有就没得录, 但有些场景下可能不想录制音频,只想录视频,所以增加个开关
  *
  *@param is_record_audio: 1 表示录制音频, 0 表示不录制音频, 默认是1
  *
  * @return {0} if successful
  */
  public native int SmartPlayerSetRecorderAudio(long handle, int is_record_audio);
  /**
   * Start recorder stream(开始录像)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @return {0} if successful
   */
  public native int SmartPlayerStartRecorder(long handle);
  /**
   * Stop recorder stream(停止录像)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @return {0} if successful
   */
  public native int SmartPlayerStopRecorder(long handle);

15. 拉流回调编码后的数据(配合转发模块使用)

拉流回调编码后的数据,主要是为了配合转发模块使用,比如拉取rtsp流数据,直接转RTMP推送到RTMP服务。

  /*
   * 设置拉流时音频转AAC编码的开关
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * aac比较通用,sdk增加其他音频编码(比如speex, pcmu, pcma等)转aac的功能.
   *
   * @param is_transcode: 设置为1的话,如果音频编码不是aac,则转成aac, 如果是aac,则不做转换. 设置为0的话,则不做任何转换. 默认是0.
   * 注意: 转码会增加性能消耗
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetPullStreamAudioTranscodeAAC(long handle, int is_transcode);
  /**
   * Start pull stream(开始拉流,用于数据转发,只拉流不播放)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @return {0} if successful
   */
  public native int SmartPlayerStartPullStream(long handle);
  /**
   * Stop pull stream(停止拉流)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @return {0} if successful
   */
  public native int SmartPlayerStopPullStream(long handle);
  /**
   * Set Audio Data Callback(设置回调编码后音频数据)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param audio_data_callback: Audio Data Callback.
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetAudioDataCallback(long handle, Object audio_data_callback);
  /**
   * Set Video Data Callback(设置回调编码后视频数据)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param video_data_callback: Video Data Callback.
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetVideoDataCallback(long handle, Object video_data_callback);

16. H264用户数据回调或SEI数据回调

如发送端在264编码时,加了自定义的user data数据,可以通过以下接口实现数据回调,如需直接回调SEI数据,调下面SEI回调接口即可。

  /**
   * Set user data Callback(设置回调SEI扩展用户数据)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param user_data_callback: user data callback.
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetUserDataCallback(long handle, Object user_data_callback);
  /**
   * Set SEI data Callback(设置回调SEI数据)
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param sei_data_callback: sei data callback.
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetSEIDataCallback(long handle, Object sei_data_callback);

17. 设置回调解码后YUV、RGB数据

如需对解码后的yuv或rgb数据,进行二次处理,如人脸识别等,可以通回调yuv rgb接口实现数据二次处理。


以YUV为例:

    class I420ExternalRender implements NTExternalRender {
        // public static final int NT_FRAME_FORMAT_RGBA = 1;
        // public static final int NT_FRAME_FORMAT_ABGR = 2;
        // public static final int NT_FRAME_FORMAT_I420 = 3;
        private int width_ = 0;
        private int height_ = 0;
        private int y_row_bytes_ = 0;
        private int u_row_bytes_ = 0;
        private int v_row_bytes_ = 0;
        private ByteBuffer y_buffer_ = null;
        private ByteBuffer u_buffer_ = null;
        private ByteBuffer v_buffer_ = null;
        @Override
        public int getNTFrameFormat() {
            Log.i(TAG, "I420ExternalRender::getNTFrameFormat return "
                    + NT_FRAME_FORMAT_I420);
            return NT_FRAME_FORMAT_I420;
        }
        @Override
        public void onNTFrameSizeChanged(int width, int height) {
            width_ = width;
            height_ = height;
            y_row_bytes_ = (width_ + 15) & (~15);
            u_row_bytes_ = ((width_ + 1) / 2 + 15) & (~15);
            v_row_bytes_ = ((width_ + 1) / 2 + 15) & (~15);
            y_buffer_ = ByteBuffer.allocateDirect(y_row_bytes_ * height_);
            u_buffer_ = ByteBuffer.allocateDirect(u_row_bytes_
                    * ((height_ + 1) / 2));
            v_buffer_ = ByteBuffer.allocateDirect(v_row_bytes_
                    * ((height_ + 1) / 2));
            Log.i(TAG, "I420ExternalRender::onNTFrameSizeChanged width_="
                    + width_ + " height_=" + height_ + " y_row_bytes_="
                    + y_row_bytes_ + " u_row_bytes_=" + u_row_bytes_
                    + " v_row_bytes_=" + v_row_bytes_);
        }
        @Override
        public ByteBuffer getNTPlaneByteBuffer(int index) {
            if (index == 0) {
                return y_buffer_;
            } else if (index == 1) {
                return u_buffer_;
            } else if (index == 2) {
                return v_buffer_;
            } else {
                Log.e(TAG, "I420ExternalRender::getNTPlaneByteBuffer index error:" + index);
                return null;
            }
        }
        @Override
        public int getNTPlanePerRowBytes(int index) {
            if (index == 0) {
                return y_row_bytes_;
            } else if (index == 1) {
                return u_row_bytes_;
            } else if (index == 2) {
                return v_row_bytes_;
            } else {
                Log.e(TAG, "I420ExternalRender::getNTPlanePerRowBytes index error:" + index);
                return 0;
            }
        }
        public void onNTRenderFrame(int width, int height, long timestamp) {
            if (y_buffer_ == null)
                return;
            if (u_buffer_ == null)
                return;
            if (v_buffer_ == null)
                return;
            y_buffer_.rewind();
            u_buffer_.rewind();
            v_buffer_.rewind();
        /*
        if ( !is_saved_image )
        {
          is_saved_image = true;
          int y_len = y_row_bytes_*height_;
          int u_len = u_row_bytes_*((height_+1)/2);
          int v_len = v_row_bytes_*((height_+1)/2);
          int data_len = y_len + (y_row_bytes_*((height_+1)/2));
          byte[] nv21_data = new byte[data_len];
          byte[] u_data = new byte[u_len];
          byte[] v_data = new byte[v_len];
          y_buffer_.get(nv21_data, 0, y_len);
          u_buffer_.get(u_data, 0, u_len);
          v_buffer_.get(v_data, 0, v_len);
          int[] strides = new int[2];
          strides[0] = y_row_bytes_;
          strides[1] = y_row_bytes_;
          int loop_row_c = ((height_+1)/2);
          int loop_c = ((width_+1)/2);
          int dst_row = y_len;
          int src_v_row = 0;
          int src_u_row = 0;
          for ( int i = 0; i < loop_row_c; ++i)
          {
            int dst_pos = dst_row;
            for ( int j = 0; j <loop_c; ++j )
            {
              nv21_data[dst_pos++] = v_data[src_v_row + j];
              nv21_data[dst_pos++] = u_data[src_u_row + j];
            }
            dst_row   += y_row_bytes_;
            src_v_row += v_row_bytes_;
            src_u_row += u_row_bytes_;
          }
          String imagePath = "/sdcard" + "/" + "testonv21" + ".jpeg";
          Log.e(TAG, "I420ExternalRender::begin test save iamge++ image_path:" + imagePath);
          try
          {
            File file = new File(imagePath);
              FileOutputStream image_os = new FileOutputStream(file);
              YuvImage image = new YuvImage(nv21_data, ImageFormat.NV21, width_, height_, strides);
              image.compressToJpeg(new android.graphics.Rect(0, 0, width_, height_), 50, image_os);
              image_os.flush();
              image_os.close();
          }
          catch(IOException e)
          {
            e.printStackTrace();
          }
          Log.e(TAG, "I420ExternalRender::begin test save iamge--");
        }
        */
            Log.i(TAG, "I420ExternalRender::onNTRenderFrame w=" + width + " h=" + height + " timestamp=" + timestamp);
            // copy buffer
            // test
            // byte[] test_buffer = new byte[16];
            // y_buffer_.get(test_buffer);
            // Log.i(TAG, "I420ExternalRender::onNTRenderFrame y data:" + bytesToHexString(test_buffer));
            // u_buffer_.get(test_buffer);
            // Log.i(TAG, "I420ExternalRender::onNTRenderFrame u data:" + bytesToHexString(test_buffer));
            // v_buffer_.get(test_buffer);
            // Log.i(TAG, "I420ExternalRender::onNTRenderFrame v data:" + bytesToHexString(test_buffer));
        }
    }

18. 设置视频画面填充模式

设置视频画面的填充模式,如填充整个view、等比例填充view,如不设置,默认填充整个view。


相关接口设计如下:

  /**
   * 设置视频画面的填充模式,如填充整个view、等比例填充view,如不设置,默认填充整个view
   * @param handle: return value from SmartPlayerOpen()
   * @param render_scale_mode 0: 填充整个view; 1: 等比例填充view, 默认值是0
   * @return {0} if successful
   */
  public native int SmartPlayerSetRenderScaleMode(long handle, int render_scale_mode);

19. 设置SurfaceView模式下render类型

format: 0: RGB565格式,如不设置,默认此模式; 1: ARGB8888格式

  /**
   * 设置SurfaceView模式下(NTRenderer.CreateRenderer第二个参数传false的情况),render类型
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param format: 0: RGB565格式,如不设置,默认此模式; 1: ARGB8888格式
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetSurfaceRenderFormat(long handle, int format);

20. 设置surfaceview模式下抗锯齿

需要注意的是:抗锯齿模式开启后,会增加性能消耗。

  /**
   * 设置SurfaceView模式下(NTRenderer.CreateRenderer第二个参数传false的情况),抗锯齿效果,注意:抗锯齿模式开启后,可能会影像性能,请慎用
   *
   * @param handle: return value from SmartPlayerOpen()
   *
   * @param isEnableAntiAlias: 0: 如不设置,默认不开启抗锯齿模式; 1: 开启抗锯齿模式
   *
   * @return {0} if successful
   */
  public native int SmartPlayerSetSurfaceAntiAlias(long handle, int isEnableAntiAlias);

总结

以上就是Android平台RTSP、RTMP播放器接口设计需要参考的点,对于大多数开发者来说,不一定需要实现上述所有部分,只要按照产品诉求,实现其中的40%就足够满足特定场景使用了。


一个好的播放器,特别是要满足低延迟稳定的播放(毫秒级延迟),需要注意的点远不止如此,感兴趣的开发者,可以参考blog其他文章。

相关文章
|
1月前
|
Java Android开发 Swift
安卓与iOS开发对比:平台选择对项目成功的影响
【10月更文挑战第4天】在移动应用开发的世界中,选择合适的平台是至关重要的。本文将深入探讨安卓和iOS两大主流平台的开发环境、用户基础、市场份额和开发成本等方面的差异,并分析这些差异如何影响项目的最终成果。通过比较这两个平台的优势与挑战,开发者可以更好地决定哪个平台更适合他们的项目需求。
110 1
|
2月前
|
IDE Android开发 iOS开发
探索Android与iOS开发的差异:平台选择对项目成功的影响
【9月更文挑战第27天】在移动应用开发的世界中,Android和iOS是两个主要的操作系统平台。每个系统都有其独特的开发环境、工具和用户群体。本文将深入探讨这两个平台的关键差异点,并分析这些差异如何影响应用的性能、用户体验和最终的市场表现。通过对比分析,我们将揭示选择正确的开发平台对于确保项目成功的重要作用。
|
23天前
|
Java 程序员 开发工具
Android|修复阿里云播放器下载不回调的问题
虽然 GC 带来了很多便利,但在实际编码时,我们也需要注意对象的生命周期管理,该存活的存活,该释放的释放,避免因为 GC 导致的问题。
28 2
|
1月前
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
82 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
Android开发 数据安全/隐私保护 开发工具
|
5天前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
7天前
|
Android开发 Swift iOS开发
探索安卓与iOS开发的差异和挑战
【10月更文挑战第37天】在移动应用开发的广阔舞台上,安卓和iOS这两大操作系统扮演着主角。它们各自拥有独特的特性、优势以及面临的开发挑战。本文将深入探讨这两个平台在开发过程中的主要差异,从编程语言到用户界面设计,再到市场分布的不同影响,旨在为开发者提供一个全面的视角,帮助他们更好地理解并应对在不同平台上进行应用开发时可能遇到的难题和机遇。
|
9天前
|
XML 存储 Java
探索安卓开发之旅:从新手到专家
【10月更文挑战第35天】在数字化时代,安卓应用的开发成为了一个热门话题。本文旨在通过浅显易懂的语言,带领初学者了解安卓开发的基础知识,同时为有一定经验的开发者提供进阶技巧。我们将一起探讨如何从零开始构建第一个安卓应用,并逐步深入到性能优化和高级功能的实现。无论你是编程新手还是希望提升技能的开发者,这篇文章都将为你提供有价值的指导和灵感。
|
7天前
|
存储 API 开发工具
探索安卓开发:从基础到进阶
【10月更文挑战第37天】在这篇文章中,我们将一起探索安卓开发的奥秘。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和建议。我们将从安卓开发的基础开始,逐步深入到更复杂的主题,如自定义组件、性能优化等。最后,我们将通过一个代码示例来展示如何实现一个简单的安卓应用。让我们一起开始吧!
|
8天前
|
存储 XML JSON
探索安卓开发:从新手到专家的旅程
【10月更文挑战第36天】在这篇文章中,我们将一起踏上一段激动人心的旅程,从零基础开始,逐步深入安卓开发的奥秘。无论你是编程新手,还是希望扩展技能的老手,这里都有适合你的知识宝藏等待发掘。通过实际的代码示例和深入浅出的解释,我们将解锁安卓开发的关键技能,让你能够构建自己的应用程序,甚至贡献于开源社区。准备好了吗?让我们开始吧!
21 2