​​Android平台GB28181历史视音频文件下载规范探讨及技术实现

简介: ​​Android平台GB28181历史视音频文件下载规范探讨及技术实现

技术背景

上篇blog,我们提到了Android平台GB28181历史视音频文件检索规范探讨及技术实现,文件检索后,GB28181平台侧,可以针对文件列表进行回放或下载操作,本文主要探讨视音频文件下载相关。

规范解读

视音频文件下载基本要求

SIP 服务器接收到媒体接收者发送的视音频文件下载请求后向媒体流发送者发送媒体文件下载命令,媒体流发送者采用RTP将视频流传输给媒体流接收者,媒体流接收者直接将视频流保存为媒体文件。

媒体流接收者可以是用户客户端或联网系统,媒体流发送者可以是媒体设备或联网系统。媒体流接收者或 SIP 服务器可通过配置查询等方式获取媒体流发送者支持的下载发送倍速,并在请求的 SDP 消息体中携带指定下载倍速。

媒体流发送者可在 Invite 请求对应的 200 0K 响应 SDP 消息体中扩展携带下载文件的大小参数,以便于媒体流接收者计算下载进度,当媒体流发送者不能提供文件大小参数时,媒体流接收者应支持根据码流中取得的时间计算下载进度。视音频文件下载宜支持媒体流保活机制。

命令流程

视音频文件下载流程.png

其中,信令 1,8,9、10,11,12 为 SIP 服务器接收到客户端的呼叫请求后通过 B2BUA 代理方式建立媒体流接受者与媒体服务器之间的媒体链接信令过程。

信令 2~7 为 SIP 服务器通过三方呼叫控制建立媒体服务器与媒体流之间的媒体链接信令过程。

信令 13~16 为媒体流发送者回放下载到文件结束向媒体接收者发送下载完成的通知消息过程。

信令 17~20 为媒体流接收者断开与媒体服务器之间的媒体链接信令过程。

信令 21~24 为 SIP 服务器断开媒体服务器与媒体流发送者之间的媒体链接信令过程。

命令流程描述如下:

    1. 媒体流接收者向 SIP 服务器发送Invite 消息,消息头域中携带 Subject 字段,表明点播的视频源 ID、发送方媒体流序列号、媒体流接收者 ID、接收端媒体流序列号标识等参数,SDP 消息体中s字段为“Download”代表文件下载,u字段代表下载通道 ID 和下载类型,字段代表下载时间段,可扩展 a 字段携带下载倍速参数,规定此次下载设备发流倍速,若不携带默认为1 倍速。
    2. SIP 服务器收到 Invite 请求后,通过三方呼叫控制建立媒体服务器和媒体流发送者之间的媒体连接。向媒体服务器发送 Invite 消息,此消息不携带 SDP 消息体。
    3. 媒体服务器收到 SIP 服务器的 Invite 请求后,回复 200 0K 响应,携带 SDP 消息体,消息体中描述了媒体服务器接收媒体流的 IP端口、媒体格式等内容。
    4. SIP 服务器收到媒体服务器返回的 200 OK响应后,向媒体流发送者发送 Invite请求,请求中携带消息 3 中媒体服务器回复的 200 OK响应消息体。s字段为“Download”代表文件下载,u字段代表下载通道 ID 和下载类型,t字段代表下载时间段,增加y字段描述 SSRC 值,f字段描述媒体参数,可扩展 a 字段携带下载倍速,将倍速参数传递给设备。
    5. 媒体流发送者收到 SIP 服务器的 Invite 请求后,回复 200 OK响应,携带 SDP消息体,消息体中描述了媒体流发送者发送媒体流的IP、端口、媒体格式、SSRC 字段等内容,可扩展 a 字段携带文件大小参数。
    6. SIP 服务器收到媒体流发送者返回的 200 OK响应后,向媒体服务器发送 ACK 请求,请求中携带消息 5 中媒体流发送者回复的 200 OK响应消息体,完成与媒体服务器的 Invite 会话建立过程。
    7. SIP 服务器收到媒体流发送者返回的 200 OK响应后,向媒体流发送者发送 ACK 请求,请求中不携带消息体,完成与媒体流发送者的 Invite 会话建立过程。
    8. 完成三方呼叫控制后,SIP 服务器通过 B2BUA 代理方式建立媒体流接收者和媒体服务器之间的媒体连接。在消息 1 中增加 SSRC 值,转发给媒体服务器。
    9. 媒体服务器收到 Invite 请求,回复 200 OK响应,携带 SDP 消息体,消息体中描述了媒体服务器发送媒体流的IP、端口、媒体格式、SSRC 值等内容。
    10. SIP 服务器将消息 9 转发给媒体流接收者,可扩展 a 字段携带文件大小参数。
    11. 媒体流接收者收到 200 OK响应后,回复 ACK 消息,完成与 SIP 服务器的 Invite 会话建立过程。
    12. SIP 服务器将消息 11 转发给媒体服务器,完成与媒体服务器的 Invite 会话建立过程。
    13. 媒体流发送者在文件下载结束后发送会话内 Message 消息。
    14. SIP 服务器收到消息 17 后转发给媒体流接收者。
    15. 媒体流接收者收到消息 18 后回复 200 OK响应,进行链路断开过程。
    16. SIP 服务器将消息 19 转发给媒体流发送者。
    17. 媒体流接收者向 SIP 服务器发送 BYE 消息,断开消息1、10、11建立的同媒体流接收者的Invite 会话。
    18. SIP服务器收到 BYE消息后回复200OK 响应,会话断开。
    19. SIP 服务器收到 BYE 消息后向媒体服务器发送 BYE 消息,断开消息 8,9,12 建立的同媒体服务器的 Invite 会话。
    20. 媒体服务器收到 BYE 消息后回复 200 OK 响应,会话断开。
    21. SIP 服务器向媒体服务器发送 BYE 消息,断开消息 2,3,6 建立的同媒体服务器的 Invite 会话。
    22. 媒体服务器收到 BYE 消息后回复 200 OK响应,会话断开。
    23. SIP 服务器向媒体流发送者发送 BYE 消息,断开消息 4,5,7 建立的同媒体流发送者的Invite 会话。
    24. 媒体流发送者收到 BYE 消息后回复 200 OK响应,会话断开。

    技术实现

    本文以大牛直播SDK开发的Android平台GB28181设备接入侧视音频历史文件检索和下载为例(本文侧重于下载),介绍下相关设计思路:

    camera2.jpg

    Android设备接入端收到国标平台侧发过来的INVITE SDP:

    v=0o=3402000000138000000100 IN IP4 192.168.2.154
    s=Download
    u=34020000001380000001:0
    c=IN IP4 192.168.2.154
    t=16937964261693796703m=video 30002 RTP/AVP 969798a=recvonly
    a=rtpmap:96 PS/90000
    a=rtpmap:97 MPEG4/90000
    a=rtpmap:98 H264/90000
    a=downloadspeed:4
    y=1200000001

    image.gif

    上述SDP里面,s=Download表示系下载,a=downloadspeed:4 表示4倍速下载,SSRC是:1200000001(SSRC第1位为历史或实时媒体流的标识位,其中0为实时视音频,1为历史视音频)。

    Android设备接入端回复:

    v=0o=3402000001131000003900 IN IP4 192.168.2.212
    s=Download
    c=IN IP4 192.168.2.212
    t=00m=video 36576 RTP/AVP 96a=rtpmap:96 PS/90000
    a=filesize:15611511
    a=sendonly
    y=1200000001

    image.gif

    a=filesize:15611511表示录像文件大小是15611511Byte,携带文件大小参数, 便于媒体流接收者计算下载进度(a=filesize是整个媒体容器的大小和实际发送的音视频帧总字节数有一定差异)。

    国标平台侧发Ack后,开始下载视音频数据,下载过程中,可以通过SIP-INFO消息和MANSRTSP协议调节下载倍速:

    PLAY RTSP/1.0
    CSeq: 31129Scale: 0.25

    image.gif

    Android GB28181设备接入侧发送完音视频帧后,发送通知事件类型"121", 表示历史媒体文件发送结束,发送会话内Message消息如下:

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

    image.gif

    接口设计

    信令接口设计:

    /*** Author: daniusdk.com*/packagecom.gb.ntsignalling;
    publicinterfaceGBSIPAgent {
    voidaddDownloadListener(GBSIPAgentDownloadListenerdownloadListener);
    voidremoveDownloadListener(GBSIPAgentDownloadListenerremoveListener);
    /**响应Invite Download 200 OK*/booleanrespondDownloadInviteOK(longid, StringdeviceId, StringstartTime, StringstopTime, MediaSessionDescriptionlocalMediaDescription);
    /**响应Invite Download 其他状态码*/booleanrespondDownloadInvite(intstatusCode, longid, StringdeviceId, StringstartTime, StringstopTime);
    /** 媒体流发送者在文件下载结束后发Message消息通知SIP服务器回文件已发送完成* notifyType 必须是"121“*/booleannotifyDownloadMediaStatus(longid, StringdeviceId, StringstartTime, StringstopTime, StringnotifyType);
    /**终止Download会话*/voidterminateDownload(longid, StringdeviceId, StringstartTime, StringstopTime, booleanisSendBYE);
    /**终止所有Download会话*/voidterminateAllDownloads(booleanisSendBYE);
    }

    image.gif

    历史视音频下载listener设计:

    /*** Author: daniusdk.com*/packagecom.gb.ntsignalling;
    publicinterfaceGBSIPAgentDownloadListener {
    /**收到s=Download的文件下载Invite*/voidntsOnInviteDownload(longid, StringdeviceId, SessionDescriptionsessionDescription);
    /**发送Download invite response 异常*/voidntsOnDownloadInviteResponseException(longid, StringdeviceId, StringstartTime, StringstopTime, intstatusCode, StringerrorInfo);
    /** 收到CANCEL Download INVITE请求*/voidntsOnCancelDownload(longid, StringdeviceId, StringstartTime, StringstopTime);
    /** 收到Ack*/voidntsOnAckDownload(longid, StringdeviceId, StringstartTime, StringstopTime);
    /** 更改下载速度*/voidntsOnDownloadMANSRTSPScaleCommand(longid, StringdeviceId, StringstartTime, StringstopTime, doublescale);
    /** 收到Bye*/voidntsOnByeDownload(longid, StringdeviceId, StringstartTime, StringstopTime);
    /** 不是在收到BYE Message情况下, 终止Download*/voidntsOnTerminateDownload(longid, StringdeviceId, StringstartTime, StringstopTime);
    /** Download会话对应的对话终止, 一般不会触发这个回调,目前只有在响应了200K, 但在64*T1时间后还没收到ACK,才可能会出发收到这个, 请做相关清理处理*/voidntsOnDownloadDialogTerminated(longid, StringdeviceId, StringstartTime, StringstopTime);
    }

    image.gif

    底层jni接口设计:

    /*** SmartPublisherJniV2.java* Author: daniusdk.com*/packagecom.daniulive.smartpublisher;
    publicclassSmartPublisherJniV2 {
    /*** 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*/publicnativelongSmartPublisherOpen(Objectctx, intaudio_opt, intvideo_opt,  intwidth, intheight);
    /*** 设置流类型* @param type: 0:表示 live 流, 1:表示 on-demand 流, SDK默认为0(live流)* 注意: 流类型设置当前仅对GB28181媒体流有效* @return {0} if successful*/publicnativeintSetStreamType(longhandle, inttype);
    /*** 投递视频 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*/publicnativeintPostVideoOnDemandPacketByteBuffer(longhandle, intcodec_id,
    ByteBufferpacket, intoffset, intsize, longpts_us, intis_pts_discontinuity, intis_key,
    byte[] codec_specific_data, intcodec_specific_data_size,
    intwidth, intheight);
    /*** 投递音频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*/publicnativeintPostAudioOnDemandPacketByteBuffer(longhandle, intcodec_id,
    ByteBufferpacket, intoffset, intsize, longpts_us, intis_pts_discontinuity,
    byte[] codec_specific_data, intcodec_specific_data_size,
    intsample_rate, intchannels);
    /*** 启动 GB28181 媒体流** @return {0} if successful*/publicnativeintStartGB28181MediaStream(longhandle);
    /*** 停止 GB28181 媒体流** @return {0} if successful*/publicnativeintStopGB28181MediaStream(longhandle);
    /*** 关闭推送实例,结束时必须调用close接口释放资源** @return {0} if successful*/publicnativeintSmartPublisherClose(longhandle);
    }

    image.gif

    上次处理逻辑

    RecordDownloadListenerImpl实现如下:

    /*** RecordDownloadListenerImpl.java* Author: daniusdk.com*/packagecom.daniulive.smartpublisher;
    publicclassRecordDownloadListenerImplimplementscom.gb.ntsignalling.GBSIPAgentDownloadListener {
    /**收到s=Download的文件下载Invite*/@OverridepublicvoidntsOnInviteDownload(longid, StringdeviceId, SessionDescriptionsdp) {
    if (!post_task(newOnInviteTask(this.context_, this.is_exit_, this.senders_map_, deviceId, sdp, id))) {
    Log.e(TAG, "ntsOnInviteDownload post_task failed, "+RecordSender.make_print_tuple(id, deviceId, sdp.getTime().getStartTime(),  sdp.getTime().getStopTime()));
    // 这里不发488, 等待事务超时也可以的GBSIPAgentagent=this.context_.get_agent();
    if (agent!=null)
    agent.respondDownloadInvite(488, id, deviceId, sdp.getTime().getStartTime(), sdp.getTime().getStopTime());
            }
        }
    /** 收到CANCEL Download INVITE请求*/@OverridepublicvoidntsOnCancelDownload(longid, StringdeviceId, StringstartTime, StringstopTime) {
    Log.i(TAG, "ntsOnCancelDownload, "+RecordSender.make_print_tuple(id, deviceId, startTime, stopTime));
    RecordSendersender=senders_map_.remove(id);
    if (null==sender)
    return;
    StopDisposeTasktask=newStopDisposeTask(sender);
    if (!post_task(task))
    task.run();
        }
    /** 收到Ack*/@OverridepublicvoidntsOnAckDownload(longid, StringdeviceId, StringstartTime, StringstopTime) {
    Log.i(TAG, "ntsOnAckDownload, "+RecordSender.make_print_tuple(id, deviceId, startTime, stopTime));
    RecordSendersender=senders_map_.get(id);
    if (null==sender) {
    Log.e(TAG, "ntsOnAckDownload get sender is null, "+RecordSender.make_print_tuple(id, deviceId, startTime, stopTime));
    GBSIPAgentagent=this.context_.get_agent();
    if (agent!=null)
    agent.terminateDownload(id, deviceId, startTime, stopTime, false);
    return;
            }
    StartTasktask=newStartTask(sender, this.senders_map_);
    if (!post_task(task))
    task.run();
        }
    /** 收到Bye*/@OverridepublicvoidntsOnByeDownload(longid, StringdeviceId, StringstartTime, StringstopTime) {
    Log.i(TAG, "ntsOnByeDownload, "+RecordSender.make_print_tuple(id, deviceId, startTime, stopTime));
    RecordSendersender=this.senders_map_.remove(id);
    if (null==sender)
    return;
    StopDisposeTasktask=newStopDisposeTask(sender);
    if (!post_task(task))
    task.run();
        }
    /** 更改下载速度*/@OverridepublicvoidntsOnDownloadMANSRTSPScaleCommand(longid, StringdeviceId, StringstartTime, StringstopTime, doublescale) {
    if (scale<0.01) {
    Log.e(TAG, "ntsOnDownloadMANSRTSPScaleCommand invalid scale:"+scale+" "+RecordSender.make_print_tuple(id, deviceId, startTime, stopTime));
    return;
            }
    RecordSendersender=this.senders_map_.get(id);
    if (null==sender) {
    Log.e(TAG, "ntsOnDownloadMANSRTSPScaleCommand can not get sender, scale:"+scale+" "+RecordSender.make_print_tuple(id, deviceId, startTime, stopTime));
    return;
            }
    sender.set_speed(scale);
    Log.i(TAG, "ntsOnDownloadMANSRTSPScaleCommand, scale:"+scale+" "+RecordSender.make_print_tuple(id, deviceId, startTime, stopTime));
        }
    }

    image.gif

    文件发送相关处理代码如下:

    /*** RecordSender.java* Author: daniusdk.com*/packagecom.daniulive.smartpublisher;
    publicclassRecordSender {
    publicvoidset_speed(doublespeed) {
    intpercent_speed= (int)(speed*100);
    this.percent_speed_.set(percent_speed);
        }
    publicvoidset_file_description(RecordFileDescriptiondesc) {
    this.file_description_=desc;
        }
    publicstaticStringmake_print_tuple(longid, Stringdevice_id, Stringstart_time, Stringstop_time) {
    StringBuildersb=newStringBuilder(96);
    sb.append("[id:").append(id);
    sb.append(", device:"+device_id);
    sb.append(", t=").append(start_time).append(" ").append(start_time);
    sb.append("]");
    returnsb.toString();
        }
    publicbooleanstart() {
    SendThreadcurrent_thread=thread_.get();
    if (current_thread!=null) {
    if (current_thread.is_exit()) {
    Log.e(TAG, "start, the thread already exists and has exited, return false, "+get_print_tuple());
    returnfalse;
                }
    Log.i(TAG, "start, the thread already exists and has exited, return true, "+get_print_tuple());
    returntrue;
            }
    SendThreadthread=newSendThread();
    if (!thread_.compareAndSet(null, thread)) {
    Log.i(TAG, "start, call compareAndSet return false, the thread already exists, return true, "+get_print_tuple());
    returntrue;
            }
    try {
    Log.i(TAG, "start thread, "+get_print_tuple());
    thread.start();
            }catch (Exceptione) {
    thread_.compareAndSet(thread, null);
    Log.e(TAG, "start e:", e);
    returnfalse;
            }
    returntrue;
        }
    publicvoidstop() {
    SendThreadcurrent_thread=thread_.get();
    if (current_thread!=null&&!current_thread.is_exit()) {
    current_thread.exit();
    Log.i(TAG, "stop, exit thread "+get_print_tuple());
            }
        }
    privatebooleaninit_native_sender(StackDisposabledisposables) {
    if(native_handle_!=0) {
    Log.e(TAG, "init_native_sender, native_handle_ is not 0, "+get_print_tuple());
    returnfalse;
            }
    if (null==this.media_info_||!this.media_info_.is_has_track() ) {
    Log.e(TAG, "init_native_sender, there is no track, "+get_print_tuple());
    returnfalse;
            }
    if (0==rtp_handle_) {
    Log.e(TAG, "init_native_sender, rtp_handle_ is 0, "+get_print_tuple());
    returnfalse;
            }
    if (null==lib_publisher_){
    Log.e(TAG, "init_native_sender, lib_publisher_ is null, "+get_print_tuple());
    returnfalse;
            }
    Contextcontext=this.context_.get_context();
    if (null==context) {
    Log.e(TAG, "init_native_sender, context is null, "+get_print_tuple());
    returnfalse;
            }
    longhandle=lib_publisher_.SmartPublisherOpen(context, media_info_.is_has_audio_track()?2:0, media_info_.is_has_video_track()?2:0, 0, 0);
    if (0==handle) {
    Log.e(TAG, "init_native_sender, call SmartPublisherOpen failed, "+get_print_tuple());
    returnfalse;
            }
    NativeSenderDisposablenative_disposable=newNativeSenderDisposable(lib_publisher_, handle);
    lib_publisher_.SetStreamType(handle, 1);
    List<MediaTrack>tracks=media_info_.get_tracks();
    for (MediaTracki : tracks) {
    if (i.is_video())
    lib_publisher_.SetEncodedVideoCodecId(handle, i.codec_id(), i.csd_set(), i.csd_set() !=null?i.csd_set().length : 0);
    elseif(i.is_audio())
    lib_publisher_.SetEncodedAudioCodecId(handle, i.codec_id(), i.csd_set(), i.csd_set() !=null?i.csd_set().length : 0);
            }
    lib_publisher_.SetGB28181RTPSender(handle, rtp_handle_, rtp_payload_type_, rtp_encoding_name_);
    intret=lib_publisher_.StartGB28181MediaStream(handle);
    if (ret!=0) {
    Log.e(TAG, "init_native_sender, call StartGB28181MediaStream failed, "+get_print_tuple());
    native_disposable.dispose();
    returnfalse;
            }
    native_disposable.is_need_call_stop(true);
    disposables.push(native_disposable);
    native_handle_=handle;
    returntrue;
        }
    privatebooleanpost_media_packet(MediaPacketpacket) {
    /*Log.i(TAG, "post "+ MediaTrack.get_media_type_string(packet.media_type()) + " " +MediaTrack.get_codec_id_string(packet.codec_id()) + " packet, pts:" + out_point_3(packet.pts_us()/1000.0) +"ms, key:"+ (packet.is_key()?1:0) + ", size:" + packet.size()); */if (null==lib_publisher_||0==native_handle_||!packet.is_has_data())
    returnfalse;
    if (packet.is_audio()) {
    if (packet.is_aac()) {
    if (packet.is_has_codec_specific_data_set())
    return0==lib_publisher_.PostAudioOnDemandPacketByteBuffer(native_handle_, packet.codec_id(), packet.data(), 0, packet.size(),
    packet.pts_us(), 0, packet.codec_specific_data_set(), packet.codec_specific_data_set_size(), 0, 0);
                }
            }elseif (packet.is_video()) {
    if (packet.is_avc() ||packet.is_hevc()) {
    return0==lib_publisher_.PostVideoOnDemandPacketByteBuffer(native_handle_, packet.codec_id(), packet.data(),
    0, packet.size(), packet.pts_us(), 0, packet.is_key()?1:0,
    packet.codec_specific_data_set(), packet.codec_specific_data_set_size(), 0, 0);
                }
            }
    returnfalse;
        }
    privatevoidrelease_packets(Deque<MediaPacket>packets) {
    while (!packets.isEmpty())
    packets.removeFirst().release_buffer();
        }
    privatestaticStringout_point_3(doublev) { returnString.format("%.3f", v); }
    publicstaticStringto_mega_bytes_string(longbytes) {
    doublemb=bytes/(1024*1024.0);
    returnout_point_3(mb);
        }
    privateclassSendThreadextendsThread {
    @Overridepublicvoidrun() {
    /****相关代码**/        }
        }
    }

    image.gif

    总结

    GB28181历史视音频文件下载,看似逻辑复杂,实际上也不简单,文件下载是在完成录像和历史视音频文件检索的基础上,分别从信令、RTP数据打包发送等角度实现,考虑到录像文件的完整性,历史视音频文件下载的设计目标是减少丢帧丢包,推荐使用RTP over TCP模式,尽管作为GB28181设备接入侧,我们尽可能按照标准规范来实现,实际对接的国标平台厂商,多少会有些差异,具体还要根据现场实际情况酌情处理。

    相关实践学习
    深入解析Docker容器化技术
    Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。 &nbsp; &nbsp; 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
    相关文章
    |
    11月前
    |
    监控 Android开发 数据安全/隐私保护
    批量发送短信的平台,安卓群发短信工具插件脚本,批量群发短信软件【autojs版】
    这个Auto.js脚本实现了完整的批量短信发送功能,包含联系人管理、短信内容编辑、发送状态监控等功能
    |
    存储 编解码 监控
    Android平台GB28181执法记录仪技术方案与实现
    本文介绍了大牛直播SDK的SmartGBD在执法记录仪场景中的应用。GB28181协议作为视频监控联网的国家标准,为设备互联互通提供规范。SmartGBD专为Android平台设计,支持音视频采集、编码与传输,具备自适应算法和多功能扩展优势。文章分析了执法记录仪的需求,如实时音视频传输、设备管理及数据安全,并详细阐述了基于SmartGBD的技术实现方案,包括环境准备、SDK集成、设备注册、音视频处理及功能扩展等步骤。最后展望了SmartGBD在未来智慧物联领域的广阔应用前景。
    943 13
    |
    存储 编解码 开发工具
    Android平台毫秒级低延迟HTTP-FLV直播播放器技术探究与实现
    本文详细探讨了在Android平台上实现HTTP-FLV播放器的过程。首先介绍了FLV格式的基础,包括文件头和标签结构。接着分析了HTTP-FLV传输原理,通过分块传输实现流畅播放。然后重点讲解了播放器的实现步骤,涵盖网络请求、数据解析、音视频解码与渲染,以及播放控制功能的设计。文章还讨论了性能优化和网络异常处理的方法,并总结了HTTP-FLV播放器的技术价值,尤其是在特定场景下的应用意义。
    804 11
    |
    监控 Shell Linux
    Android调试终极指南:ADB安装+多设备连接+ANR日志抓取全流程解析,覆盖环境变量配置/多设备调试/ANR日志分析全流程,附Win/Mac/Linux三平台解决方案
    ADB(Android Debug Bridge)是安卓开发中的重要工具,用于连接电脑与安卓设备,实现文件传输、应用管理、日志抓取等功能。本文介绍了 ADB 的基本概念、安装配置及常用命令。包括:1) 基本命令如 `adb version` 和 `adb devices`;2) 权限操作如 `adb root` 和 `adb shell`;3) APK 操作如安装、卸载应用;4) 文件传输如 `adb push` 和 `adb pull`;5) 日志记录如 `adb logcat`;6) 系统信息获取如屏幕截图和录屏。通过这些功能,用户可高效调试和管理安卓设备。
    10376 2
    |
    8月前
    |
    移动开发 前端开发 Android开发
    【02】建立各项目录和页面标准化产品-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
    【02】建立各项目录和页面标准化产品-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
    1487 12
    【02】建立各项目录和页面标准化产品-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
    |
    8月前
    |
    移动开发 JavaScript 应用服务中间件
    【06】优化完善落地页样式内容-精度优化-vue加vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
    【06】优化完善落地页样式内容-精度优化-vue加vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
    1055 5
    【06】优化完善落地页样式内容-精度优化-vue加vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
    |
    8月前
    |
    移动开发 Rust JavaScript
    【01】首页建立-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
    【01】首页建立-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
    1138 4
    【01】首页建立-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
    |
    9月前
    |
    开发工具 Android开发
    X Android SDK file not found: adb.安卓开发常见问题-Android SDK 缺少 `adb`(Android Debug Bridge)-优雅草卓伊凡
    X Android SDK file not found: adb.安卓开发常见问题-Android SDK 缺少 `adb`(Android Debug Bridge)-优雅草卓伊凡
    895 11
    X Android SDK file not found: adb.安卓开发常见问题-Android SDK 缺少 `adb`(Android Debug Bridge)-优雅草卓伊凡
    |
    8月前
    |
    移动开发 Android开发
    【03】建立隐私关于等相关页面和内容-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
    【03】建立隐私关于等相关页面和内容-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
    398 0
    |
    9月前
    |
    Java 开发工具 Maven
    【01】完整的安卓二次商业实战-详细的初级步骤同步项目和gradle配置以及开发思路-优雅草伊凡
    【01】完整的安卓二次商业实战-详细的初级步骤同步项目和gradle配置以及开发思路-优雅草伊凡
    1223 6

    热门文章

    最新文章