首页 > 其他分享 >Android GB28181历史视音频远程回放

Android GB28181历史视音频远程回放

时间:2023-11-01 17:02:10浏览次数:36  
标签:task sender GB28181 long 视音频 deviceId Android id String

  作为GB28181安卓客户端,实时视音频点播是必须支持的功能,对于历史视音频回放功能,不支持的话可以从设备上拷贝录像文件再播放,但有些场景没法拷贝,安卓支持回放还是需要的。

  历史视音频的回放和实时视音频点播信令上很相似,音视频数据都是通过RTP传输,信令回放要处理SIP INFO消息,解析MANSRTSP协议,实现快进、慢放、暂停、停止、位置拖动等远程控制命令。

  GB28181文档详细定义了历史视音频的回放的信令流程, 对于安卓设备端的实现, 需要实现媒体流发送者相关的信令:

1:SIP服务器向安卓设备发送Invite请求,请求中携带SDP信息,SDP中的s字段为“Playback”代表历史回放,u字段代表回放通道ID和回放类型,t字段代表回放时间段,增加y字段描述 SSRC 值,f字段描述媒体参数。

2:安卓设备收到SIP服务器的Invite请求后,回复200OK响应(回复200 OK等最终响应前,也可先回复一个临时响应,比如180 Ringing等),携带SDP消息体, SDP中描述了安卓设备发送媒体流的IP、端口、媒体格式、SSRC字段等内容。

3:SIP服务器收到安卓设备返回的200OK响应后,向安卓设备发送ACK请求,请求中不携带消息体,完成与安卓设备的Invite会话建立过程。

4:安卓设备按Invite SDP中给出的IP地址和端口等信息,发送音视频RTP包(推荐PS RTP包)到媒体服务器。

5:在回放过程中,播放端通过向SIP服务器发送会话内Info+MANSRTSP消息(SIP服务器再转发给安卓设备端)进行回放控制,包括视频的暂停、播放、快放、慢放、随机拖放播放等操作。

6:安卓设备端在文件回放结束后发送会话内Message消息,通知SIP服务器回放已结束(会话内消息请参考RFC3261-12.2 Requests within a Dialog)。

7:SIP服务器收到媒体通知消息后做相应的处理,之后SIP服务器向安卓端发送BYE消息。

8:安卓设备收到BYE消息后回复200OK响应,会话断开,释放相关资源。

  为方便快速上手代码实现,下面给出信令的具体实例:

  安卓设备收到的INVITE SDP:

v=0
o=64010000041310000137 0 0 IN IP4 192.168.0.193
s=Playback
u=64010000041310000137:0
c=IN IP4 192.168.0.193
t=1698218951 1698219270
m=video 20072 RTP/AVP 96

a=recvonly
a=rtpmap:96 PS/90000
y=1900000005

  s=Playback代表历史回放,SSRC是:1900000005(SSRC第1位为历史或实时媒体流的标识位,0为实时,1为历史).

  安卓设备回复200 OK携带的SDP:

v=0
o=64010000041310000137 0 0 IN IP4 192.168.0.151
s=MyAndroidPlaybackTest
c=IN IP4 192.168.0.151
t=0 0
m=video 36010 RTP/AVP 96
a=rtpmap:96 PS/90000
a=sendonly
y=1900000005

  回放过程中通过SIP INFO消息+MANSRTSP协议将正常播放速度变为4倍快进播放:  

PLAY RTSP/1.0
CSeq: 110379
Scale: 4.000000

  回放过程中使用SIP INFO消息+MANSRTSP协议把播放位置改为60.27秒的位置(考虑到安卓一般使用微秒或纳秒单位,转换到微秒是:60270000): 

PLAY RTSP/1.0
CSeq: 110392
Range:npt=60.27-

  文件回放结束后安卓设备端发送会话内Message消息: 

<?xml version="1.0" encoding="GB2312"?>
<Notify>
<CmdType>MediaStatus</CmdType>
<SN>1738772385</SN>
<DeviceID>64010000041310000137</DeviceID>
<NotifyType>121</NotifyType>
</Notify>

  通知事件类型是"121", 表示历史媒体文件发送结束。 

  相关实现代码:

/**
* 部分信令接口
*/

package com.gb.ntsignalling;

public interface GBSIPAgent {
    void addPlaybackListener(GBSIPAgentPlaybackListener playbackListener);

    void removePlaybackListener(GBSIPAgentPlaybackListener playbackListener);

    /*
     *响应Invite Playback 200 OK
     */
    boolean respondPlaybackInviteOK(long id, String deviceId, String startTime, String stopTime, MediaSessionDescription localMediaDescription);

    /*
     *响应Invite Playback 其他状态码
     */
    boolean respondPlaybackInvite(int statusCode, long id, String deviceId);

    /*
     * 媒体流发送者在回放结束后发Message消息通知SIP服务器回放文件已发送完成
     * notifyType 必须是"121"
     */
    boolean notifyPlaybackMediaStatus(long id, String deviceId, String notifyType);

    /*
     *终止Playback会话
     */
    void terminatePlayback(long id, String deviceId, boolean isSendBYE);

    /*
     *终止所有Playback会话
     */
    void terminateAllPlaybacks(boolean isSendBYE);
}


/**
* 信令Playback Listener
*/
package com.gb.ntsignalling;

public interface GBSIPAgentPlaybackListener {
    /*
     *收到s=Playback的历史回放Invite
     */
    void ntsOnInvitePlayback(long id, String deviceId, SessionDescription sessionDescription);

    /*
     *发送Playback invite response 异常
     */
    void ntsOnPlaybackInviteResponseException(long id, String deviceId, int statusCode, String errorInfo);

    /*
     * 收到CANCEL Playback INVITE请求
     */
    void ntsOnCancelPlayback(long id, String deviceId);

    /*
     * 收到Ack
     */
    void ntsOnAckPlayback(long id, String deviceId);

    /*
    * 播放命令
     */
    void ntsOnPlaybackMANSRTSPPlayCommand(long id, String deviceId);

    /*
     * 暂停命令
     */
    void ntsOnPlaybackMANSRTSPPauseCommand(long id, String deviceId);

    /*
     * 快进/慢进命令
     */
    void ntsOnPlaybackMANSRTSPScaleCommand(long id, String deviceId, double scale);

    /*
     * 随机拖动命令
     */
    void ntsOnPlaybackMANSRTSPSeekCommand(long id, String deviceId, double position_sec);

    /*
     * 停止命令
     */
    void ntsOnPlaybackMANSRTSPTeardownCommand(long id, String deviceId);

    /*
     * 收到Bye
     */
    void ntsOnByePlayback(long id, String deviceId);

    /*
     * 不是在收到BYE Message情况下, 终止Playback
     */
    void ntsOnTerminatePlayback(long id, String deviceId);

    /*
     * Playback会话对应的对话终止, 一般不会触发这个回调,目前只有在响应了200K, 但在64*T1时间后还没收到ACK,才可能会出发
    收到这个, 请做相关清理处理
    */
    void ntsOnPlaybackDialogTerminated(long id, String deviceId);
}


/**
* 部分JNI接口, rtp ps 打包发送等代码C++实现
*/
 
public class SmartPublisherJniV2 {
 
     /**
	 * Open publisher(启动推送实例)
	 *
	 * @param ctx: get by this.getApplicationContext()
	 * 
	 * @param audio_opt:
	 * if 0: 不推送音频
	 * if 1: 推送编码前音频(PCM)
	 * if 2: 推送编码后音频(aac/pcma/pcmu/speex).
	 * 
	 * @param video_opt:
	 * if 0: 不推送视频
	 * if 1: 推送编码前视频(NV12/I420/RGBA8888等格式)
	 * if 2: 推送编码后视频(AVC/HEVC)
	 * if 3: 层叠加模式
	 *
	 * <pre>This function must be called firstly.</pre>
	 *
	 * @return the handle of publisher instance
	 */
    public native long SmartPublisherOpen(Object ctx, int audio_opt, int video_opt,  int width, int height);
 
    
     /**
	 * 设置流类型
	 * @param type: 0:表示 live 流, 1:表示 on-demand 流, SDK默认为0(live流)
	 * 注意: 流类型设置当前仅对GB28181媒体流有效
	 * @return {0} if successful
	 */
    public native int SetStreamType(long handle, int type);
 
 
    /**
	 * 投递视频 on demand包, 当前只用于GB28181推送, 注意ByteBuffer对象必须是DirectBuffer
	 *
	 * @param codec_id: 编码id, 当前支持H264和H265, 1:H264, 2:H265
	 *
	 * @param packet: 视频数据, 包格式请参考H264/H265 Annex B Byte stream format, 例如:
	 *                0x00000001 nal_unit 0x00000001 ...
	 *                H264 IDR: 0x00000001 sps 0x00000001 pps 0x00000001 IDR_nal_unit .... 或 0x00000001 IDR_nal_unit ....
	 *                H265 IDR: 0x00000001 vps 0x00000001 sps 0x00000001 pps 0x00000001 IDR_nal_unit .... 或 0x00000001 IDR_nal_unit ....
	 *
	 * @param offset: 偏移量
	 * @param size: packet size
	 * @param pts_us: 时间戳, 单位微秒
	 * @param is_pts_discontinuity: 是否时间戳间断,0:未间断,1:间断
	 * @param is_key: 是否是关键帧, 0:非关键帧, 1:关键帧
	 * @param codec_specific_data: 可选参数,可传null, 对于H264关键帧包, 如果packet不含sps和pps, 可传0x00000001 sps 0x00000001 pps
	 *                    ,对于H265关键帧包, 如果packet不含vps,sps和pps, 可传0x00000001 vps 0x00000001 sps 0x00000001 pps
	 * @param codec_specific_data_size: codec_specific_data size
	 * @param width: 图像宽, 可传0
	 * @param height: 图像高, 可传0
	 *
	 * @return {0} if successful
	 */
	public native int PostVideoOnDemandPacketByteBuffer(long handle, int codec_id,
														ByteBuffer packet, int offset, int size, long pts_us, int is_pts_discontinuity, int is_key,
														byte[] codec_specific_data, int codec_specific_data_size,
									  					int width, int height);
 
	
     /**
	 * 投递音频on demand包, 当前只用于GB28181推送, 注意ByteBuffer对象必须是DirectBuffer
	 *
	 * @param codec_id: 编码id, 当前支持PCMA和AAC, 65536:PCMA, 65538:AAC
	 * @param packet: 音频数据
	 * @param offset:packet偏移量
	 * @param size: packet size
	 * @param pts_us: 时间戳, 单位微秒
	 * @param is_pts_discontinuity: 是否时间戳间断,0:未间断,1:间断
	 * @param codec_specific_data: 如果是AAC的话,需要传 Audio Specific Configuration
	 * @param codec_specific_data_size: codec_specific_data size
	 * @param sample_rate: 采样率
	 * @param channels: 通道数
	 *
	 * @return {0} if successful
	 */
	public native int PostAudioOnDemandPacketByteBuffer(long handle, int codec_id,
														ByteBuffer packet, int offset, int size, long pts_us, int is_pts_discontinuity,
														byte[] codec_specific_data, int codec_specific_data_size,
														int sample_rate, int channels);
																											
	/**
	 * on demand source完成seek后, 请调用
	 * @return {0} if successful
	 */
	public native int OnSeekProcessed(long handle);
 
	/**
	 * 启动 GB28181 媒体流
	 *
	 * @return {0} if successful
	 */
	public native int StartGB28181MediaStream(long handle);
 
 
    /**
	 * 停止 GB28181 媒体流
	 *
	 * @return {0} if successful
	 */
	public native int StopGB28181MediaStream(long handle);
 
    
	/**
     * 关闭推送实例,结束时必须调用close接口释放资源
	 *
	 * @return {0} if successful
	 */
    public native int SmartPublisherClose(long handle);
 
}


/**
* Listener部分实现代码
*/

public class PlaybackListenerImpl implements com.gb.ntsignalling.GBSIPAgentPlaybackListener {
 
    /*
     *收到s=Playback的文件下载Invite
     */
    @Override
    public void ntsOnInvitePlayback(long id, String deviceId, SessionDescription sdp) {
        if (!post_task(new PlaybackListenerImpl.OnInviteTask(this.context_, this.is_exit_, this.senders_map_, deviceId, sdp, id))) {
            Log.e(TAG, "ntsOnInvitePlayback post_task failed, " + RecordSender.make_print_tuple(id, deviceId, sdp.getTime().getStartTime(),  sdp.getTime().getStopTime()));

            // 这里不发488, 等待事务超时也可以的
            GBSIPAgent agent = this.context_.get_agent();
            if (agent != null)
                agent.respondPlaybackInvite(488, id, deviceId);
        }
    }

    /*
     *发送Playback invite response 异常
     */
    @Override
    public void ntsOnPlaybackInviteResponseException(long id, String deviceId, int statusCode, String errorInfo) {
        Log.i(TAG, "ntsOnPlaybackInviteResponseException, status_code:" + statusCode + ", "
                + RecordSender.make_print_tuple(id, deviceId) + ",  error_info:" + errorInfo);

        RecordSender sender = senders_map_.remove(id);
        if (null == sender)
            return;

        PlaybackListenerImpl.StopDisposeTask task = new PlaybackListenerImpl.StopDisposeTask(sender);
        if (!post_task(task))
            task.run();
    }

    /*
     * 收到CANCEL Playback INVITE请求
     */
    @Override
    public void ntsOnCancelPlayback(long id, String deviceId) {
        Log.i(TAG, "ntsOnCancelPlayback, " + RecordSender.make_print_tuple(id, deviceId));

        RecordSender sender = senders_map_.remove(id);
        if (null == sender)
            return;

        PlaybackListenerImpl.StopDisposeTask task = new PlaybackListenerImpl.StopDisposeTask(sender);
        if (!post_task(task))
            task.run();
    }

    /*
     * 收到Ack
     */
    @Override
    public void ntsOnAckPlayback(long id, String deviceId) {
        Log.i(TAG, "ntsOnAckPlayback, "+ RecordSender.make_print_tuple(id, deviceId));

        RecordSender sender = senders_map_.get(id);
        if (null == sender) {
            Log.e(TAG, "ntsOnAckPlayback get sender is null, " + RecordSender.make_print_tuple(id, deviceId));

            GBSIPAgent agent = this.context_.get_agent();
            if (agent != null)
                agent.terminatePlayback(id, deviceId, false);

            return;
        }

        PlaybackListenerImpl.StartTask task = new PlaybackListenerImpl.StartTask(sender, this.senders_map_);
        if (!post_task(task))
            task.run();
    }

    /*
     * 收到Bye
     */
    @Override
    public void ntsOnByePlayback(long id, String deviceId) {
        Log.i(TAG, "ntsOnByePlayback, "+ RecordSender.make_print_tuple(id, deviceId));

        RecordSender sender = this.senders_map_.remove(id);
        if (null == sender)
            return;

        PlaybackListenerImpl.StopDisposeTask task = new PlaybackListenerImpl.StopDisposeTask(sender);
        if (!post_task(task))
            task.run();
    }

    /*
     * 播放命令
     */
    @Override
    public void ntsOnPlaybackMANSRTSPPlayCommand(long id, String deviceId) {
        RecordSender sender = this.senders_map_.get(id);
        if (null == sender) {
            Log.e(TAG, "ntsOnPlaybackMANSRTSPPlayCommand can not get sender " + RecordSender.make_print_tuple(id, deviceId));
            return;
        }

        sender.post_play_command();

        Log.i(TAG, "ntsOnPlaybackMANSRTSPPlayCommand " + RecordSender.make_print_tuple(id, deviceId));
    }

    /*
     * 暂停命令
     */
    @Override
    public void ntsOnPlaybackMANSRTSPPauseCommand(long id, String deviceId) {
        RecordSender sender = this.senders_map_.get(id);
        if (null == sender) {
            Log.e(TAG, "ntsOnPlaybackMANSRTSPPauseCommand can not get sender " + RecordSender.make_print_tuple(id, deviceId));
            return;
        }

        sender.post_pause_command();

        Log.i(TAG, "ntsOnPlaybackMANSRTSPPauseCommand " + RecordSender.make_print_tuple(id, deviceId));
    }

    /*
     * 快进/慢进命令
     */
    @Override
    public void ntsOnPlaybackMANSRTSPScaleCommand(long id, String deviceId, double scale) {
        if (scale < 0.01) {
            Log.e(TAG, "ntsOnPlaybackMANSRTSPScaleCommand invalid scale:" + scale  + " " + RecordSender.make_print_tuple(id, deviceId));
            return;
        }

        RecordSender sender = this.senders_map_.get(id);
        if (null == sender) {
            Log.e(TAG, "ntsOnPlaybackMANSRTSPScaleCommand can not get sender, scale:" + scale  + " " + RecordSender.make_print_tuple(id, deviceId));
            return;
        }

        sender.post_scale_command(scale);

        Log.i(TAG, "ntsOnPlaybackMANSRTSPScaleCommand, scale:" + scale + " " + RecordSender.make_print_tuple(id, deviceId));
    }

    /*
     * 随机拖动命令
     */
    @Override
    public void ntsOnPlaybackMANSRTSPSeekCommand(long id, String device_id, double position_sec) {
        if (position_sec < 0.0) {
            Log.e(TAG, "ntsOnPlaybackMANSRTSPSeekCommand invalid seek pos:" + position_sec  + ", " + RecordSender.make_print_tuple(id, device_id));
            return;
        }

        RecordSender sender = this.senders_map_.get(id);
        if (null == sender) {
            Log.e(TAG, "ntsOnPlaybackMANSRTSPSeekCommand can not get sender " + RecordSender.make_print_tuple(id, device_id));
            return;
        }

        long offset_ms = sender.get_file_start_time_offset_ms();
        position_sec += (offset_ms/1000.0);

        sender.post_seek_command(position_sec);

        Log.i(TAG, "ntsOnPlaybackMANSRTSPSeekCommand seek pos:" + RecordSender.out_point_3(position_sec) + "s, " + RecordSender.make_print_tuple(id, device_id));
    }

    /*
     * 停止命令
     */
    @Override
    public void ntsOnPlaybackMANSRTSPTeardownCommand(long id, String device_id) {
        CallTerminatePlaybackTask call_terminate_task =  new CallTerminatePlaybackTask(this.context_, id, device_id, true);
        post_task(call_terminate_task);

        RecordSender sender = this.senders_map_.remove(id);
        if (null == sender) {
            Log.w(TAG, "ntsOnPlaybackMANSRTSPTeardownCommand can not remove sender " + RecordSender.make_print_tuple(id, device_id));
            return;
        }

        Log.i(TAG, "ntsOnPlaybackMANSRTSPTeardownCommand " + RecordSender.make_print_tuple(id, device_id));

        PlaybackListenerImpl.StopDisposeTask task = new PlaybackListenerImpl.StopDisposeTask(sender);
        if (!post_task(task))
            task.run();
    }

    /*
     * 不是在收到BYE Message情况下, 终止Playback
     */
    @Override
    public void ntsOnTerminatePlayback(long id, String deviceId) {
        Log.i(TAG, "ntsOnTerminatePlayback, "+ RecordSender.make_print_tuple(id, deviceId));

        RecordSender sender = this.senders_map_.remove(id);
        if (null == sender)
            return;

        PlaybackListenerImpl.StopDisposeTask task = new PlaybackListenerImpl.StopDisposeTask(sender);
        if (!post_task(task))
            task.run();
    }

    /*
     * Playback会话对应的对话终止, 一般不会触发这个回调,目前只有在响应了200K, 但在64*T1时间后还没收到ACK,才可能会出发
    收到这个, 请做相关清理处理
    */
    @Override
    public void ntsOnPlaybackDialogTerminated(long id, String deviceId) {
        Log.i(TAG, "ntsOnPlaybackDialogTerminated, "+ RecordSender.make_print_tuple(id, deviceId));

        RecordSender sender = this.senders_map_.remove(id);
        if (null == sender)
            return;

        PlaybackListenerImpl.StopDisposeTask task = new PlaybackListenerImpl.StopDisposeTask(sender);
        if (!post_task(task))
            task.run();
    }
}

  安卓GB28181历史视音频的回放实现代码较多,大体上分为三块,信令部分,rtp打包发送部分,文件检索发送等逻辑,  有些用Java实现有些用C++, 这里为了方便说明信令流程和关键细节, 只给出部分接口定义和实现代码。

  安卓设备录像文件远程回放实现代码细节较多, 处理起来也繁琐,建议能把录像文件从设备中拷出来播就拷吧。另外回放的音视频传输强烈推荐使用RTP over TCP(具体请参考RFC 4571)。

标签:task,sender,GB28181,long,视音频,deviceId,Android,id,String
From: https://blog.51cto.com/u_15527397/8129896

相关文章

  • GB28181安防监控LiteCVR存储录像到指定新硬盘,如何自动挂载该磁盘?
    当前各行各业的现代化管理需要运用先进的科学技术手段,将电子技术与计算机控制集成在一个完整的视频监控系统中,利用现有的成熟先进的监控保安设备和系统架构,可有效加强对各种场合,特殊设备以及人员的管理,直观及时的反映重要地点、区域的现场情况。近期有用户想咨询在本地部署Lite......
  • Hbuilderx运行uni-app项目到Android Studio模拟器只显示“同步手机端程序文件完成”界
    如图,开发工具也显示同步文件,模拟器也显示同步文件完成,但是就是不展示页面,遇到这种情况,一般是2种情况,一个是项目本身有问题跑不起来,另一个就是创建的模拟器设备参数不支持当前app。一.连接真机调试,排除项目本身问题:如果连接真机都跑不起来,那么看下控制台日志,先解决项目本身的问......
  • 设备或平台通过GB28181协议接入上级平台后在线,却不能实播该怎么处理?
    设备或平台通过GB28181协议接入上级平台后在线了,却不能实播该怎么处理?1.查看摄像机或平台的视频类型  目前GB28181协议只支持H.264或MPEG-4编码格式,详细可参考:https://www.cnblogs.com/wsmei/p/17772807.html 在摄像机修改方法以海康为例:    以大华为例:   2......
  • Android自动化测试框架:UiAutomator和UiAutomator2的区别与示例代码
    UiAutomator和UiAutomator2是两种常用的Android自动化测试框架,它们都是由Google开发的。然而,它们之间存在一些关键的区别:API级别:UiAutomator框架在Android4.3(API级别18)中引入,而UiAutomator2在Android5.0(API级别21)中引入。测试能力:UiAutomator只能测试Android系统应用......
  • 关于Android桌面小组件相关的开发,涉及到的一些点
    你可能用过一些AndroidAPP的小组件,比如:支付宝的小组件:之前疫情期间添加了对应小组件卡片在桌面,可点击小卡片上的查看健康码的按钮,可一键打开健康码。音乐类APP的小组件:添加对应对应小组件后,可在APP的主屏幕中轻松看到当前播放歌曲的相关信息:歌曲封面、歌曲名、歌手名称、所......
  • 19万字Android Framework面试通关秘籍,打破技术壁垒
    2023年已经接近尾声了,疫情的影响也在逐渐减小,市场慢慢复苏。不过最近还是会有一些,“Android市场饱和了”、“大环境还是不好”、“投几十个简历都没有一个约面的”的声音。其实并不是岗位需求量变少了,是越来越多的公司需要【中、高级Android工程师】。企业的用人需求越来越高,面试通......
  • 掌握《Android Framework源码开发揭秘》,成为移动开发领域的领跑者
    前言前两天被一条消息给震惊到了:阿里上半年裁员超1.36万人,今年将新增近6000名应届大学生。差点以为阿里扛不住了。。。。裁员这个事大家应该见怪不怪,这两年,我们已经被一波又一波的裁员浪潮,冲激得可以说是麻木了,但是1.36万这个数字还是挺吓人的。对于企业来说,这是调整经营策略、优化......
  • Android Studio 新项目没有layout
    说明今天安装完新版本的AndroidStudio后,新建项目发现没有layout文件夹,网上搜索得知,原来是官方新增了选项。调整后IntelliJIDEA2023.2.1之前的版本,EmptyActivity是指EmptyViewActivity,而现在EmptyActivity是指EmptyComposeActivity,另外多了一个EmptyViewActiv......
  • android ebpf中的CO-RE学习
    CO-RE原理因为不同的内核版本的系统内部结构体会有差异,例如structuser_arg_ptr,当内核编译配置中存在CONFIG_COMPAT=y的时候,会在native成员之前增加一个布尔变量is_compat,这样native的偏移就发生的变化。如果编写的ebpf内核程序需要访问structuser_arg_ptr类型的变量就需要考......
  • Android之WebView显示PDF文档
    参考:https://blog.csdn.net/Android_Cll/article/details/131641229https://cloud.tencent.com/developer/article/2301730Android项目新增js:/app/src/main/assets/wwwroot/index.js我新建了一个wwwroot放里面了。自己看着办。varurl=location.search.substring(1);PDFJS.......