​​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设备接入侧,我们尽可能按照标准规范来实现,实际对接的国标平台厂商,多少会有些差异,具体还要根据现场实际情况酌情处理。

    相关实践学习
    容器服务Serverless版ACK Serverless 快速入门:在线魔方应用部署和监控
    通过本实验,您将了解到容器服务Serverless版ACK Serverless 的基本产品能力,即可以实现快速部署一个在线魔方应用,并借助阿里云容器服务成熟的产品生态,实现在线应用的企业级监控,提升应用稳定性。
    云原生实践公开课
    课程大纲 开篇:如何学习并实践云原生技术 基础篇: 5 步上手 Kubernetes 进阶篇:生产环境下的 K8s 实践 相关的阿里云产品:容器服务&nbsp;ACK 容器服务&nbsp;Kubernetes&nbsp;版(简称&nbsp;ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情:&nbsp;https://www.aliyun.com/product/kubernetes
    相关文章
    |
    2月前
    |
    Android开发
    安卓SO层开发 -- 编译指定平台的SO文件
    安卓SO层开发 -- 编译指定平台的SO文件
    32 0
    |
    1月前
    |
    运维 监控 Java
    应用研发平台EMAS产品常见问题之安卓构建版本失败如何解决
    应用研发平台EMAS(Enterprise Mobile Application Service)是阿里云提供的一个全栈移动应用开发平台,集成了应用开发、测试、部署、监控和运营服务;本合集旨在总结EMAS产品在应用开发和运维过程中的常见问题及解决方案,助力开发者和企业高效解决技术难题,加速移动应用的上线和稳定运行。
    |
    1月前
    |
    运维 监控 Android开发
    应用研发平台EMAS常见问题之安卓push的离线转通知目前无法收到如何解决
    应用研发平台EMAS(Enterprise Mobile Application Service)是阿里云提供的一个全栈移动应用开发平台,集成了应用开发、测试、部署、监控和运营服务;本合集旨在总结EMAS产品在应用开发和运维过程中的常见问题及解决方案,助力开发者和企业高效解决技术难题,加速移动应用的上线和稳定运行。
    25 1
    |
    2月前
    |
    人工智能 vr&ar Android开发
    探索安卓与iOS系统的技术进展
    【2月更文挑战第4天】本文将探讨安卓与iOS两大操作系统在最新技术进展方面的差异与相似之处。我们将分析它们在人工智能、增强现实、隐私保护等方面的创新和发展,并展望未来可能出现的趋势。通过对比这两个操作系统的技术特点,读者将能够更好地了解并选择适合自己需求的智能设备。
    |
    3月前
    |
    安全 算法 JavaScript
    安卓逆向 -- 关键代码定位与分析技术
    安卓逆向 -- 关键代码定位与分析技术
    43 0
    |
    6天前
    |
    Linux 编译器 Android开发
    FFmpeg开发笔记(九)Linux交叉编译Android的x265库
    在Linux环境下,本文指导如何交叉编译x265的so库以适应Android。首先,需安装cmake和下载android-ndk-r21e。接着,下载x265源码,修改crosscompile.cmake的编译器设置。配置x265源码,使用指定的NDK路径,并在配置界面修改相关选项。随后,修改编译规则,编译并安装x265,调整pc描述文件并更新PKG_CONFIG_PATH。最后,修改FFmpeg配置脚本启用x265支持,编译安装FFmpeg,将生成的so文件导入Android工程,调整gradle配置以确保顺利运行。
    24 1
    FFmpeg开发笔记(九)Linux交叉编译Android的x265库
    |
    29天前
    |
    Java Android开发
    Android 开发获取通知栏权限时会出现两个应用图标
    Android 开发获取通知栏权限时会出现两个应用图标
    14 0
    |
    1月前
    |
    设计模式 人工智能 开发工具
    安卓应用开发:构建未来移动体验
    【2月更文挑战第17天】 随着智能手机的普及和移动互联网技术的不断进步,安卓应用开发已成为一个热门领域。本文将深入探讨安卓平台的应用开发流程、关键技术以及未来发展趋势。通过分析安卓系统的架构、开发工具和框架,本文旨在为开发者提供全面的技术指导,帮助他们构建高效、创新的移动应用,以满足不断变化的市场需求。
    18 1
    |
    3天前
    |
    数据库 Android开发 开发者
    安卓应用开发:构建高效用户界面的策略
    【4月更文挑战第24天】 在竞争激烈的移动应用市场中,一个流畅且响应迅速的用户界面(UI)是吸引和保留用户的关键。针对安卓平台,开发者面临着多样化的设备和系统版本,这增加了构建高效UI的复杂性。本文将深入分析安卓平台上构建高效用户界面的最佳实践,包括布局优化、资源管理和绘制性能的考量,旨在为开发者提供实用的技术指南,帮助他们创建更流畅的用户体验。
    |
    20天前
    |
    XML 开发工具 Android开发
    构建高效的安卓应用:使用Jetpack Compose优化UI开发
    【4月更文挑战第7天】 随着Android开发不断进化,开发者面临着提高应用性能与简化UI构建流程的双重挑战。本文将探讨如何使用Jetpack Compose这一现代UI工具包来优化安卓应用的开发流程,并提升用户界面的流畅性与一致性。通过介绍Jetpack Compose的核心概念、与传统方法的区别以及实际集成步骤,我们旨在提供一种高效且可靠的解决方案,以帮助开发者构建响应迅速且用户体验优良的安卓应用。