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

本文涉及的产品
视觉智能开放平台,图像资源包5000点
视觉智能开放平台,分割抠图1万点
视觉智能开放平台,视频资源包5000点
简介: ​​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 的基本产品能力,即可以实现快速部署一个在线魔方应用,并借助阿里云容器服务成熟的产品生态,实现在线应用的企业级监控,提升应用稳定性。
    容器应用与集群管理
    欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
    相关文章
    |
    9天前
    |
    安全 Android开发 iOS开发
    探索安卓与iOS开发的差异:平台特性与用户体验的深度对比
    在移动应用开发的广阔天地中,安卓和iOS两大平台各占半壁江山。本文旨在通过数据驱动的分析方法,深入探讨这两大操作系统在开发环境、用户界面设计及市场表现等方面的差异。引用最新的行业报告和科研数据,结合技术专家的观点,本文将提供对开发者和市场分析师均有价值的洞见。
    |
    3天前
    |
    机器学习/深度学习 人工智能 文字识别
    文本,文字扫描01,OCR文本识别技术展示,一个安卓App,一个简单的设计,文字识别可以应用于人工智能,机器学习,车牌识别,身份证识别,银行卡识别,PaddleOCR+SpringBoot+Andr
    文本,文字扫描01,OCR文本识别技术展示,一个安卓App,一个简单的设计,文字识别可以应用于人工智能,机器学习,车牌识别,身份证识别,银行卡识别,PaddleOCR+SpringBoot+Andr
    |
    3天前
    |
    Java Android开发 iOS开发
    探索安卓与iOS开发的差异:平台特性与创新潜力
    在移动应用开发的广阔天地中,安卓和iOS两大平台各占据一方。本文深入剖析了这两个操作系统的开发环境、工具、语言及市场趋势,旨在为开发者提供一个全面的比较视角。文章将基于最新的行业报告、技术论坛讨论以及专家分析,详细阐述两个平台的技术架构差异、开发成本和用户体验设计的不同点。通过数据支持的论证,揭示安卓与iOS在创新潜力上的独特优势,并探讨它们如何塑造未来的移动应用生态。
    5 0
    |
    6天前
    |
    5G Android开发 iOS开发
    探索iOS与安卓在移动操作系统领域的技术竞争与合作
    本文将深入探讨iOS和安卓这两大移动操作系统的技术竞争与合作。通过对市场份额、用户忠诚度、技术创新、生态系统建设以及安全性等方面的比较,我们将揭示这两个系统各自的优势和挑战。同时,我们还将分析它们如何通过技术合作来推动整个移动行业的发展。
    |
    9天前
    |
    算法 安全 Android开发
    新一代安卓系统:技术演进与用户体验革新
    在移动操作系统领域,安卓系统一直处于不断演进的过程中。本文探讨了新一代安卓系统的技术创新,以及这些创新如何为用户带来全新的体验和功能。
    10 0
    |
    9天前
    |
    编解码 开发工具 Android开发
    技术心得:打造自己的智能投屏体验——Android投屏开发入门
    技术心得:打造自己的智能投屏体验——Android投屏开发入门
    14 0
    |
    12天前
    |
    Java 开发工具 Android开发
    探索Android与iOS开发的差异:平台选择对项目成功的影响
    在移动应用开发的广阔天地中,Android和iOS两大平台各自占据着半壁江山。本文将深入探讨这两个平台在开发过程中的关键差异点,包括编程语言、开发工具、用户界面设计、性能优化以及市场覆盖等方面。通过对这些关键因素的比较分析,旨在为开发者提供一个清晰的指南,帮助他们根据项目需求和目标受众做出明智的平台选择。
    |
    12天前
    |
    编解码 Android开发 iOS开发
    深入探索Android与iOS开发的差异与挑战
    【6月更文挑战第24天】在移动应用开发的广阔舞台上,Android和iOS两大操作系统扮演着主角。它们各自拥有独特的开发环境、工具集、用户基础及市场策略。本文将深度剖析这两个平台的开发差异,并探讨开发者面临的挑战,旨在为即将踏入或已在移动开发领域奋斗的开发者提供一份实用指南。
    36 13
    |
    11天前
    |
    监控 Android开发 iOS开发
    探索Android与iOS开发的差异:平台、工具和用户体验的比较
    【6月更文挑战第25天】在移动应用开发的广阔天地中,Android和iOS两大平台各领风骚,它们在开发环境、工具选择及用户体验设计上展现出独特的风貌。本文将深入探讨这两个操作系统在技术实现、市场定位和用户交互方面的关键差异,旨在为开发者提供一个全景式的视图,帮助他们在面对项目决策时能够更加明智地选择适合自己项目需求的平台。
    |
    13天前
    |
    XML Java 开发工具
    Android Studio开发Android TV
    【6月更文挑战第19天】

    热门文章

    最新文章