Android 플랫폼에서 GB28181 기록 비디오 및 오디오 파일 다운로드 사양에 대한 토론 및 기술 구현

기술적 배경

지난 블로그에서 Android . 파일 검색 후 GB28181 플랫폼 측은 파일 목록에서 재생 또는 다운로드 작업을 수행할 수 있습니다. 이 기사에서는 주로 논의합니다. 비디오 및 오디오 파일 다운로드 관련.

표준해석

비디오 및 오디오 파일 다운로드를 위한 기본 요구 사항

SIP 서버는 미디어 수신기가 전송한 비디오 및 오디오 파일 다운로드 요청을 수신한 후 미디어 스트림 송신자에게 미디어 파일 다운로드 명령을 보내고, 미디어 스트림 송신자는 RTP를 사용하여 비디오 스트림을 미디어 스트림 수신기로 전송하고, 미디어는 스트림 수신기는 비디오 스트림을 미디어 스트림 수신기로 직접 전송하고 미디어 파일로 저장합니다.

미디어 스트림 수신기는 사용자 클라이언트 또는 네트워크 시스템일 수 있으며, 미디어 스트림 전송자는 미디어 장치 또는 네트워크 시스템일 수 있습니다. 미디어 스트림 수신기 또는 SIP 서버는 구성 쿼리 등을 통해 미디어 스트림 발신자가 지원하는 다운로드 및 전송 속도를 얻을 수 있으며 요청된 SDP 메시지 본문에 지정된 다운로드 속도를 전달할 수 있습니다.

미디어 스트림 송신자는 초대 요청에 해당하는 2000K 응답 SDP 메시지 본문에서 다운로드된 파일의 크기 매개변수를 확장하여 미디어 스트림 수신자가 다운로드 진행률을 계산할 수 있습니다. , 미디어 스트림 수신기 다운로더는 코드 스트림에서 얻은 시간을 기반으로 다운로드 진행률 계산을 지원해야 합니다. 비디오 및 오디오 파일 다운로드는 미디어 스트림 연결 유지 메커니즘을 지원해야 합니다.

명령 프로세스

그 중 시그널링 1, 8, 9, 10, 11, 12는 SIP 서버가 클라이언트의 통화 요청을 수신한 후 B2BUA 프록시 모드를 통해 미디어 스트림 수신자와 미디어 서버 사이에 미디어 링크를 설정하는 시그널링 프로세스입니다.

시그널링 2~7은 SIP 서버가 3자 통화 제어를 통해 미디어 서버와 미디어 스트림 간의 미디어 링크를 설정하는 시그널링 프로세스입니다.

Signaling 13~16은 미디어 스트림 송신자가 파일의 끝까지 다운로드를 재생하고 미디어 수신자에게 다운로드 완료 알림 메시지를 보내는 프로세스입니다.

Signaling 17~20은 미디어 스트림 수신기가 미디어 서버와의 미디어 링크를 끊기 위한 시그널링 프로세스입니다.

Signaling 21~24는 SIP 서버가 미디어 서버와 미디어 스트림 송신자 간의 미디어 링크를 끊기 위한 시그널링 프로세스입니다.

명령 프로세스는 다음과 같이 설명됩니다.

  1. 미디어 스트림 수신자는 SIP 서버에 Invite 메시지를 보냅니다. 메시지 헤더 필드에는 온디맨드 비디오 소스 ID, 발신자의 미디어 스트림 일련 번호, 미디어 스트림 수신자 ID, 수신 측의 미디어 스트림 일련 번호를 나타내는 제목 필드가 포함됩니다. SDP 메시지 본문의 s 필드는 "Download"로 파일 다운로드를 나타내고, u 필드는 다운로드 채널 ID 및 다운로드 유형을 나타내며, 필드는 다운로드 기간을 나타냅니다. a 필드는 확장 가능합니다. 이 다운로드를 위한 장치의 다운로드 속도를 지정하는 다운로드 속도 매개변수를 전달합니다. 전달되지 않은 경우 기본값은 1x 속도입니다.
  2. 초대 요청을 받은 후 SIP 서버는 제3자 통화 제어를 통해 미디어 서버와 미디어 스트림 발신자 간에 미디어 연결을 설정합니다. 미디어 서버에 초대 메시지를 보냅니다. 이 메시지에는 SDP 메시지 본문이 포함되어 있지 않습니다.
  3. SIP 서버로부터 Invite 요청을 받은 후 미디어 서버는 SDP 메시지 본문을 포함하여 200 0K 응답으로 응답합니다. 메시지 본문에는 미디어 서버가 미디어 스트림을 수신하기 위한 IP 포트, 미디어 형식 등을 설명합니다.
  4. 미디어 서버가 반환한 200 OK 응답을 받은 후 SIP 서버는 미디어 스트림 발신자에게 초대 요청을 보냅니다. 요청에는 메시지 3에서 미디어 서버가 응답한 200 OK 응답 메시지 본문이 포함됩니다. s 필드 "Download"는 파일 다운로드를 나타내고, u 필드는 다운로드 채널 ID 및 다운로드 유형을 나타내고, t 필드는 다운로드 기간을 나타내며, y 필드는 SSRC 값을 설명하기 위해 추가되고, f 필드는 미디어 매개변수를 설명합니다. a 필드를 확장하여 다운로드 속도를 두 배로 높일 수 있습니다. 매개변수가 장치에 전달됩니다.
  5. SIP 서버로부터 초대 요청을 받은 후 미디어 스트림 발신자는 SDP 메시지 본문을 포함하여 200 OK 응답으로 응답합니다. 메시지 본문에는 SIP 서버에서 보낸 미디어 스트림의 IP, 포트, 미디어 형식, SSRC 필드 및 기타 콘텐츠가 설명되어 있습니다. 미디어 스트림 발신자이며 확장 가능합니다. a 필드는 파일 크기 매개변수를 전달합니다.
  6. 미디어 스트림 발신자로부터 200 OK 응답을 받은 후 SIP 서버는 미디어 서버에 ACK 요청을 보냅니다. 요청에는 메시지 5에서 미디어 스트림 발신자가 응답한 200 OK 응답 메시지 본문이 포함되어 초대 세션 설정 프로세스를 완료합니다. 미디어 서버.
  7. SIP 서버는 미디어 스트림 발신자로부터 200 OK 응답을 받은 후 미디어 스트림 발신자에게 ACK 요청을 보냅니다. 요청에는 메시지 본문이 포함되지 않으며 미디어 스트림 발신자와의 Invite 세션 설정 프로세스가 완료됩니다.
  8. 3자 통화 제어가 완료된 후 SIP 서버는 B2BUA 프록시 방법을 통해 미디어 스트림 수신기와 미디어 서버 간의 미디어 연결을 설정합니다. 메시지 1에 SSRC 값을 추가하고 이를 미디어 서버로 전달합니다.
  9. 미디어 서버는 Invite 요청을 수신하고 200 OK 응답으로 응답하며 SDP 메시지 본문에는 미디어 서버가 보낸 미디어 스트림의 IP, 포트, 미디어 형식, SSRC 값 등이 설명되어 있습니다.
  10. SIP 서버는 메시지 9를 미디어 스트림 수신기로 전달하고 확장 가능한 a 필드는 파일 크기 매개변수를 전달합니다.
  11. 200 OK 응답을 받은 후 미디어 스트림 수신기는 ACK 메시지로 응답하여 SIP 서버와의 초대 세션 설정 프로세스를 완료합니다.
  12. SIP 서버는 미디어 서버와의 초대 세션 설정 프로세스를 완료하기 위해 메시지 11을 미디어 서버로 전달합니다.
  13. 미디어 스트리머는 파일 다운로드가 완료된 후 세션 중 메시지를 보냅니다.
  14. SIP 서버는 메시지 17을 수신하고 이를 미디어 스트림 수신기에 전달합니다.
  15. 메시지 18을 수신한 후 미디어 스트림 수신기는 200 OK 응답으로 응답하고 링크 연결 해제 프로세스를 수행합니다.
  16. SIP 서버는 메시지 19를 미디어 스트림 발신자에게 전달합니다.
  17. 미디어 스트림 수신기는 BYE 메시지를 SIP 서버로 전송하여 미디어 스트림 수신기와의 메시지 1, 10, 11로 설정된 Invite 세션의 연결을 끊습니다.
  18. BYE 메시지를 받은 후 SIP 서버는 200OK 응답으로 응답하고 세션 연결이 끊어집니다.
  19. SIP 서버는 BYE 메시지를 수신한 후 미디어 서버에 BYE 메시지를 보내고 메시지 8, 9, 12에 의해 설정된 미디어 서버와의 초대 세션을 끊습니다.
  20. BYE 메시지를 수신한 후 미디어 서버는 200 OK 응답으로 응답하고 세션 연결이 끊어집니다.
  21. SIP 서버는 미디어 서버에 BYE 메시지를 보내고 메시지 2, 3, 6에 의해 설정된 미디어 서버와의 초대 세션 연결을 끊습니다.
  22. BYE 메시지를 수신한 후 미디어 서버는 200 OK 응답으로 응답하고 세션 연결이 끊어집니다.
  23. SIP 서버는 미디어 스트림 발신자에게 BYE 메시지를 보내고 메시지 4, 5, 7에 의해 설정된 미디어 스트림 발신자와의 초대 세션 연결을 끊습니다.
  24. BYE 메시지를 받은 후 미디어 스트림 발신자는 200 OK 응답으로 응답하고 세션 연결이 끊어집니다.

기술적 실현

이 기사에서는 Daniu Live SDK에서 개발한 Android 플랫폼 GB28181 장치를 예로 들어 사이드 뷰 오디오 기록 파일 검색 및 다운로드에 액세스하고(이 기사는 다운로드에 중점을 둡니다) 관련 디자인 아이디어를 소개합니다.

 Android 장치 액세스 단말기는 국가 표준 플랫폼에서 전송된 INVITE SDP를 수신합니다.

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

위의 SDP에서 s=Download는 다운로드를 의미하고, a=downloadspeed:4는 4배속 다운로드를 의미하며, SSRC는 1200000001입니다(SSRC의 첫 번째 비트는 과거 또는 실시간 미디어 스트림의 식별 비트이고, 0은 실시간 비디오입니다). 및 오디오, 1은 과거 비디오 및 오디오입니다).

Android 장치 액세스 단말기는 다음과 같이 응답합니다.

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

a=filesize:15611511은 비디오 파일 크기가 15611511Byte이고 미디어 스트림 수신기가 다운로드 진행률을 계산하는 데 편리한 파일 크기 매개변수를 전달함을 나타냅니다. (a=filesize는 전체 미디어 컨테이너의 크기와 총 미디어 수입니다. 실제로 전송된 오디오 및 비디오 프레임의 바이트).

국가 표준 플랫폼은 Ack를 보낸 후 비디오 및 오디오 데이터 다운로드를 시작합니다.다운로드 과정에서 SIP-INFO 메시지와 MANSRTSP 프로토콜을 통해 다운로드 속도를 조정할 수 있습니다.

PLAY RTSP/1.0
CSeq: 31129
Scale: 0.25

Android GB28181 장치의 액세스 측에서 오디오 및 비디오 프레임 전송을 완료한 후 알림 이벤트 유형 "121"이 전송되어 기록 미디어 파일 전송이 완료되었음을 나타내며 다음과 같이 세션 내 메시지가 전송됩니다.

<?xml version="1.0" encoding="GB2312"?>
<Notify>
<CmdType>MediaStatus</CmdType>
<SN>213466963</SN>
<DeviceID>34020000001380000001</DeviceID>
<NotifyType>121</NotifyType>
</Notify>
인터페이스 디자인

신호 인터페이스 디자인:


/**
* Author: daniusdk.com
*/
package com.gb.ntsignalling;
 
public interface GBSIPAgent {
    void addDownloadListener(GBSIPAgentDownloadListener downloadListener);
 
    void removeDownloadListener(GBSIPAgentDownloadListener removeListener);
 
    /*
    *响应Invite Download 200 OK
    */
    boolean respondDownloadInviteOK(long id, String deviceId, String startTime, String stopTime, MediaSessionDescription localMediaDescription);
 
    /*
    *响应Invite Download 其他状态码
    */
    boolean respondDownloadInvite(int statusCode, long id, String deviceId, String startTime, String stopTime);
 
    /*
    * 媒体流发送者在文件下载结束后发Message消息通知SIP服务器回文件已发送完成
    * notifyType 必须是"121“
     */
    boolean notifyDownloadMediaStatus(long id, String deviceId, String startTime, String stopTime, String notifyType);
 
    /*
     *终止Download会话
     */
    void terminateDownload(long id, String deviceId, String startTime, String stopTime, boolean isSendBYE);
 
    /*
     *终止所有Download会话
     */
    void terminateAllDownloads(boolean isSendBYE);
 
}

과거의 비디오 및 오디오 다운로드 수신기 디자인:

/**
* Author: daniusdk.com
*/
package com.gb.ntsignalling;
 
public interface GBSIPAgentDownloadListener {
    /*
     *收到s=Download的文件下载Invite
     */
    void ntsOnInviteDownload(long id, String deviceId, SessionDescription sessionDescription);
 
    /*
     *发送Download invite response 异常
     */
    void ntsOnDownloadInviteResponseException(long id, String deviceId, String startTime, String stopTime, int statusCode, String errorInfo);
 
    /*
     * 收到CANCEL Download INVITE请求
     */
    void ntsOnCancelDownload(long id, String deviceId, String startTime, String stopTime);
 
    /*
     * 收到Ack
     */
    void ntsOnAckDownload(long id, String deviceId, String startTime, String stopTime);
 
    /*
    * 更改下载速度
     */
    void ntsOnDownloadMANSRTSPScaleCommand(long id, String deviceId, String startTime, String stopTime, double scale);
 
    /*
     * 收到Bye
     */
    void ntsOnByeDownload(long id, String deviceId, String startTime, String stopTime);
 
    /*
     * 不是在收到BYE Message情况下, 终止Download
     */
    void ntsOnTerminateDownload(long id, String deviceId, String startTime, String stopTime);
 
    /*
     * Download会话对应的对话终止, 一般不会触发这个回调,目前只有在响应了200K, 但在64*T1时间后还没收到ACK,才可能会出发
    收到这个, 请做相关清理处理
    */
    void ntsOnDownloadDialogTerminated(long id, String deviceId, String startTime, String stopTime);
}

기본 JNI 인터페이스 디자인:


/**
* SmartPublisherJniV2.java
* Author: daniusdk.com
*/
package com.daniulive.smartpublisher;
 
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);
 
	/**
	 * 启动 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);
 
}
마지막 처리 로직

RecordDownloadListenerImpl은 다음과 같이 구현됩니다.

/**
* RecordDownloadListenerImpl.java
* Author: daniusdk.com
*/
package com.daniulive.smartpublisher;
 
public class RecordDownloadListenerImpl implements com.gb.ntsignalling.GBSIPAgentDownloadListener {
 
    /*
     *收到s=Download的文件下载Invite
     */
    @Override
    public void ntsOnInviteDownload(long id, String deviceId, SessionDescription sdp) {
        if (!post_task(new OnInviteTask(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, 等待事务超时也可以的
            GBSIPAgent agent = this.context_.get_agent();
            if (agent != null)
                agent.respondDownloadInvite(488, id, deviceId, sdp.getTime().getStartTime(), sdp.getTime().getStopTime());
        }
    }
 
    /*
     * 收到CANCEL Download INVITE请求
     */
    @Override
    public void ntsOnCancelDownload(long id, String deviceId, String startTime, String stopTime) {
        Log.i(TAG, "ntsOnCancelDownload, " + RecordSender.make_print_tuple(id, deviceId, startTime, stopTime));
 
        RecordSender sender = senders_map_.remove(id);
        if (null == sender)
            return;
 
        StopDisposeTask task = new StopDisposeTask(sender);
        if (!post_task(task))
            task.run();
    }
 
    /*
     * 收到Ack
     */
    @Override
    public void ntsOnAckDownload(long id, String deviceId, String startTime, String stopTime) {
        Log.i(TAG, "ntsOnAckDownload, "+ RecordSender.make_print_tuple(id, deviceId, startTime, stopTime));
 
        RecordSender sender = senders_map_.get(id);
        if (null == sender) {
            Log.e(TAG, "ntsOnAckDownload get sender is null, " + RecordSender.make_print_tuple(id, deviceId, startTime, stopTime));
 
            GBSIPAgent agent = this.context_.get_agent();
            if (agent != null)
                agent.terminateDownload(id, deviceId, startTime, stopTime, false);
 
            return;
        }
 
        StartTask task = new StartTask(sender, this.senders_map_);
        if (!post_task(task))
            task.run();
    }
 
    /*
     * 收到Bye
     */
    @Override
    public void ntsOnByeDownload(long id, String deviceId, String startTime, String stopTime) {
        Log.i(TAG, "ntsOnByeDownload, "+ RecordSender.make_print_tuple(id, deviceId, startTime, stopTime));
 
        RecordSender sender = this.senders_map_.remove(id);
        if (null == sender)
            return;
 
        StopDisposeTask task = new StopDisposeTask(sender);
        if (!post_task(task))
            task.run();
    }
 
    /*
     * 更改下载速度
     */
    @Override
    public void ntsOnDownloadMANSRTSPScaleCommand(long id, String deviceId, String startTime, String stopTime, double scale) {
        if (scale < 0.01) {
            Log.e(TAG, "ntsOnDownloadMANSRTSPScaleCommand invalid scale:" + scale  + " " + RecordSender.make_print_tuple(id, deviceId, startTime, stopTime));
            return;
        }
 
        RecordSender sender = 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));
    }
}

파일 전송과 관련된 처리 코드는 다음과 같습니다.

/**
* RecordSender.java
* Author: daniusdk.com
*/
package com.daniulive.smartpublisher;
 
public class RecordSender {
    public void set_speed(double speed) {
        int percent_speed = (int)(speed*100);
        this.percent_speed_.set(percent_speed);
    }
 
    public void set_file_description(RecordFileDescription desc) {
        this.file_description_ = desc;
    }
 
    public static String make_print_tuple(long id, String device_id, String start_time, String stop_time) {
        StringBuilder sb = new StringBuilder(96);
        sb.append("[id:").append(id);
        sb.append(", device:" + device_id);
        sb.append(", t=").append(start_time).append(" ").append(start_time);
        sb.append("]");
        return sb.toString();
    }
 
    public boolean start() {
        SendThread current_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());
                return false;
            }
 
            Log.i(TAG, "start, the thread already exists and has exited, return true, " + get_print_tuple());
            return true;
        }
 
        SendThread thread = new SendThread();
        if (!thread_.compareAndSet(null, thread)) {
            Log.i(TAG, "start, call compareAndSet return false, the thread already exists, return true, " + get_print_tuple());
            return true;
        }
 
        try {
            Log.i(TAG, "start thread, " + get_print_tuple());
            thread.start();
        }catch (Exception e) {
            thread_.compareAndSet(thread, null);
            Log.e(TAG, "start e:", e);
            return false;
        }
 
        return true;
    }
 
    public void stop() {
        SendThread current_thread = thread_.get();
        if (current_thread != null && !current_thread.is_exit()) {
            current_thread.exit();
            Log.i(TAG, "stop, exit thread " + get_print_tuple());
        }
    }
 
    private boolean init_native_sender(StackDisposable disposables) {
        if(native_handle_ !=0) {
            Log.e(TAG, "init_native_sender, native_handle_ is not 0, " + get_print_tuple());
            return false;
        }
 
        if (null == this.media_info_ || !this.media_info_.is_has_track() ) {
            Log.e(TAG, "init_native_sender, there is no track, " + get_print_tuple());
            return false;
        }
 
        if (0 == rtp_handle_) {
            Log.e(TAG, "init_native_sender, rtp_handle_ is 0, " + get_print_tuple());
            return false;
        }
 
        if (null == lib_publisher_){
            Log.e(TAG, "init_native_sender, lib_publisher_ is null, " + get_print_tuple());
            return false;
        }
 
        Context context = this.context_.get_context();
        if (null == context) {
            Log.e(TAG, "init_native_sender, context is null, " + get_print_tuple());
            return false;
        }
 
        long handle = 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());
            return false;
        }
 
        NativeSenderDisposable native_disposable = new NativeSenderDisposable(lib_publisher_, handle);
 
        lib_publisher_.SetStreamType(handle, 1);
 
        List<MediaTrack> tracks = media_info_.get_tracks();
        for (MediaTrack i : tracks) {
            if (i.is_video())
                lib_publisher_.SetEncodedVideoCodecId(handle, i.codec_id(), i.csd_set(), i.csd_set() != null? i.csd_set().length : 0);
            else if(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_);
 
        int ret = lib_publisher_.StartGB28181MediaStream(handle);
        if (ret != 0) {
            Log.e(TAG, "init_native_sender, call StartGB28181MediaStream failed, " + get_print_tuple());
            native_disposable.dispose();
            return false;
        }
 
        native_disposable.is_need_call_stop(true);
        disposables.push(native_disposable);
 
        native_handle_ = handle;
 
        return true;
    }
 
    private boolean post_media_packet(MediaPacket packet) {
        /*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())
            return false;
 
        if (packet.is_audio()) {
            if (packet.is_aac()) {
                if (packet.is_has_codec_specific_data_set())
                    return 0 == 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);
            }
        }else if (packet.is_video()) {
            if (packet.is_avc() || packet.is_hevc()) {
                return 0 == 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);
            }
        }
 
        return false;
    }
 
  
    private void release_packets(Deque<MediaPacket> packets) {
        while (!packets.isEmpty())
            packets.removeFirst().release_buffer();
    }
 
    private static String out_point_3(double v) { return String.format("%.3f", v); }
 
    public static String to_mega_bytes_string(long bytes) {
        double mb = bytes/(1024*1024.0);
        return out_point_3(mb);
    }
 
    private class SendThread extends Thread {
	
        @Override
        public void run() {
            /***
			*相关代码
			**/
        }
    }
}

요약하다

GB28181 과거 비디오 및 오디오 파일 다운로드는 논리적으로 복잡해 보이지만 실제로는 간단하지 않습니다. 파일 다운로드는 시그널링, RTP 데이터 패키징 및 전송 관점에서 각각 비디오 및 과거 비디오 및 오디오 파일 검색을 완료하는 것을 기반으로 합니다. 등, 비디오 파일 무결성을 고려하여 과거 비디오 및 오디오 파일을 다운로드하는 설계 목표는 프레임 손실 및 패킷 손실을 줄이는 것입니다. RTP over TCP 모드를 사용하는 것이 좋습니다. GB28181 장비의 액세스 측면에서는 우리는 표준 사양에 따라 구현하기 위해 최선을 다하고 있으며 실제 도킹 국가 표준 플랫폼 제조업체에 따라 약간의 차이가 있을 수 있으며 세부 사항은 현장의 실제 상황에 따라 처리됩니다.

추천

출처blog.csdn.net/renhui1112/article/details/132708066