Прежде чем реализовать воспроизведение исторических видео- и аудиофайлов GB28181, мы завершили извлечение и загрузку исторических видео- и аудиофайлов. Воспроизведение исторических видео и аудио очень важно на платформе GB28181, например, на внешних устройствах, таких как записывающие устройства правоохранительных органов. Данные записи по умолчанию хранятся на стороне внешнего устройства. Если вам необходимо загрузить их на платформу для единого хранилища, вы можете не только скопировать их на рабочую станцию, но и загрузить их в командный центр через историческое видео. и аудиофайлы GB28181. Если командному центру необходимо напрямую просматривать исторические видео и аудио файлы, это также может быть достигнуто с помощью воспроизведения исторического видео и аудио GB28181.
GB28181 Основные требования для воспроизведения исторических видео- и аудиофайлов:
Основной процесс заключается в следующем:
В этой статье объединены сторона доступа к устройству GB28181 платформы Android и сторона платформы национального стандарта GB28181, чтобы объяснить основной процесс:
1. Платформа GB28181 отправляет приглашение на сторону доступа к устройству Android GB28181. Поле заголовка сообщения содержит поле «Тема», в котором указывается идентификатор источника видео по запросу, серийный номер медиапотока отправителя, идентификатор получателя медиапотока и т. д. флаги серийного номера медиапотока принимающей стороны и другие параметры. Сообщение содержит информацию SDP, поле s — «Воспроизведение» для представления исторического воспроизведения, поле u представляет идентификатор канала воспроизведения и тип воспроизведения, поле t представляет период времени воспроизведения, а поле y добавляется для описания значения SSRC. ;
2. После получения запроса на приглашение со стороны платформы национального стандарта сторона доступа устройства Android GB28181 отвечает 200OK и передает тело сообщения SDP. SDP описывает IP, порт, формат мультимедиа, поле SSRC и т. д. медиапоток, отправленный устройством Android;
3. После получения ответа 200 OK от стороны устройства Android национального стандарта сторона платформы национального стандарта отправляет запрос ACK на сторону устройства Android национального стандарта. Запрос не содержит тела сообщения и завершает процесс установления сеанса приглашения с помощью. сторона устройства Android национального стандарта;
4. Нажмите IP-адрес и информацию о порте, указанные в Invite SDP на устройстве Android GB28181, чтобы отправить пакет аудио и видео RTP (рекомендуется пакет PS RTP) на медиасервер;
5. В процессе воспроизведения проигрыватель осуществляет управление воспроизведением, отправляя внутрисеансовое сообщение Info+MANSRTSP на SIP-сервер (которое затем пересылается SIP-сервером на устройство Android), включая паузу видео, воспроизведение, быстрое воспроизведение, замедленное воспроизведение, случайное перетаскивание и т. д.;
6. Устройство Android GB28181 отправляет внутрисеансовое сообщение после завершения воспроизведения файла, чтобы уведомить SIP-сервер о завершении воспроизведения;
7. После получения сообщения мультимедийного уведомления сторона платформы национального стандарта обрабатывает его соответствующим образом, а затем сторона службы национального стандарта отправляет сообщение BYE на сторону устройства национального стандарта Android;
8. После получения сообщения BYE на стороне устройства Android GB28181 он отвечает 200 OK, отключает сеанс и освобождает соответствующие ресурсы.
Давайте поговорим о командах управления воспроизведением мультимедиа:
Команда управления воспроизведением мультимедиа завершается сообщением-запросом от клиента к серверу и ответным сообщением от сервера к клиенту. Запрос и ответ. Ответ относится к некоторым форматам сообщений запроса и ответа в протоколе RTSP (IETFRFC2326).
Команды воспроизведения мультимедиа:
Клиент отправляет сообщение запроса PLAY, чтобы запросить у сервера отправку мультимедиа. Должен поддерживаться заголовок Range, а в заголовке Range должен быть указан диапазон времени воспроизведения для воспроизведения мультимедиа в указанный период времени. Диапазон времени должен поддерживать диапазоны относительных меток времени npt и smpte.
Значение заголовка Range — «ntp=now-» и не содержит заголовка Scale, что означает, что воспроизведение возобновляется с позиции паузы с исходной скоростью.
PLAY RTSP/1.0 CSeq: 2 Range: npt= now-
Пример команды паузы воспроизведения:
PAUSERTSP/1.0 CSeq:1 PauseTime:now
Примеры команд быстрой и медленной перемотки вперед:
PLAYRTSP/1.0 CSeq:3 Scale:2.0
Пример команды случайного перетаскивания:
PLAYRTSP/1.0 CSeq:4 Range:npt=100-
Команда остановки:
Клиент отправляет сообщение запроса TEARDOWN, чтобы прекратить отправку указанного потока, завершить сеанс и освободить ресурсы.
Команда ответа:
Клиент и сервер должны поддерживать коды состояния 200, 4xx и 5xx команды ответа. См. IETFRFC2326.
Диапазон значений полей заголовка Scale и Range
Базовые значения, которые должен поддерживать заголовок Scale, — 0,25, 0,5, 1, 2 и 4.
Значение заголовка Range — это относительное значение начальной точки воспроизведения видео. Диапазон значений — от 0 до времени окончания воспроизведения видео. Параметр указывается в секундах и не может иметь отрицательное значение. Например, значение заголовка Range равно 0, что означает, что воспроизведение начинается с начальной точки; значение заголовка Range равно 100, что означает, что воспроизведение начинается через 100 секунд после начальной точки записи; Заголовок диапазона теперь отображается, что означает, что воспроизведение начинается с текущей позиции.
В этой статье в качестве примера рассматривается сторона доступа к устройству Daniu Live SDK на платформе Android GB28181. В настоящее время мы реализуем следующие функции:
На стороне устройства Android GB28181 осуществляется поиск видео:
<?xml version="1.0" encoding="GB2312"?>
<Query>
<CmdType>RecordInfo</CmdType>
<SN>564849544</SN>
<DeviceID>34020000001380000001</DeviceID>
<StartTime>2023-11-05T06:00:00</StartTime>
<EndTime>2023-11-05T12:00:00</EndTime>
<Type>all</Type>
</Query>
В списке на боковой стороне платформы GB отображается информация о полученном видеофайле:
Сторона устройства платформы Android GB28181 получила приглашение от платформы национального стандарта:
v=0
o=34020000001380000001 0 0 IN IP4 192.168.0.108
s=Playback
u=34020000001380000001:0
c=IN IP4 192.168.0.108
t=1699159500 1699161303
m=video 30014 RTP/AVP 96 97 98 99
a=recvonly
a=rtpmap:96 PS/90000
a=rtpmap:97 MPEG4/90000
a=rtpmap:98 H264/90000
a=rtpmap:99 H265/90000
y=1200000006
Сторона устройства платформы Android GB28181 отвечает на сторону платформы национального стандарта:
v=0
o=34020000011310000039 0 0 IN IP4 192.168.0.104
s=Playback
c=IN IP4 192.168.0.104
t=0 0
m=video 55584 RTP/AVP 96
a=rtpmap:96 PS/90000
a=filesize:199500000
a=sendonly
y=1200000006
Если вы хотите ускорить воспроизведение вперед на 2-кратной скорости:
PLAY RTSP/1.0
CSeq: 785427390
Scale: 2.000000
Устройство Android GB28181 на платформе Android отправляет внутрисеансовое сообщение, а тип времени уведомления — «121», что указывает на окончание отправки исторических медиафайлов:
<?xml version="1.0" encoding="GB2312"?>
<Notify>
<CmdType>MediaStatus</CmdType>
<SN>433507779</SN>
<DeviceID>34020000001380000001</DeviceID>
<NotifyType>121</NotifyType>
</Notify>
/*
* Author: daniusdk.com
*/
package com.gb.ntsignalling;
public interface GBSIPAgent {
void addPlaybackListener(GBSIPAgentPlaybackListener playbackListener);
void removePlaybackListener(GBSIPAgentPlaybackListener playbackListener);
/*
*Ответ на приглашение Playback 200 OK
*/
boolean respondPlaybackInviteOK(long id, String deviceId, String startTime, String stopTime, MediaSessionDescription localMediaDescription);
/*
*Ответ на приглашение Playback Другие коды состояния
*/
boolean respondPlaybackInvite(int statusCode, long id, String deviceId);
/*
* Отправитель медиапотока отправляет сообщение-сообщение после завершения воспроизведения, чтобы уведомить SIP-сервер о том, что файл воспроизведения был отправлен.
* notifyType Должно быть "121"
*/
boolean notifyPlaybackMediaStatus(long id, String deviceId, String notifyType);
/*
* Завершить сеанс воспроизведения
*/
void terminatePlayback(long id, String deviceId, boolean isSendBYE);
/*
* Завершить все сеансы воспроизведения
*/
void terminateAllPlaybacks(boolean isSendBYE);
}
/**
* Сигнализация Воспроизведение Listener
*/
package com.gb.ntsignalling;
public interface GBSIPAgentPlaybackListener {
/*
*Получено приглашение на историческое воспроизведение от s=Playback.
*/
void ntsOnInvitePlayback(long id, String deviceId, SessionDescription sessionDescription);
/*
*Отправить воспроизведение invite response аномальный
*/
void ntsOnPlaybackInviteResponseException(long id, String deviceId, int statusCode, String errorInfo);
/*
* ОТМЕНА получено Playback ПРИГЛАСИТЕ запрос
*/
void ntsOnCancelPlayback(long id, String deviceId);
/*
* Подтверждение получено
*/
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);
/*
* Получено до свидания
*/
void ntsOnByePlayback(long id, String deviceId);
/*
* Не получаю ПОКА В случае сообщения Прекратить воспроизведение
*/
void ntsOnTerminatePlayback(long id, String deviceId);
/*
* Разговор, соответствующий сеансу воспроизведения, завершается, Обычно этот обратный вызов не запускается. В настоящее время он отвечает только на 200 КБ. Но если вы не получили ACK по истечении времени 64*T1, вы можете начинать.
получил это, Пожалуйста, сделайте соответствующую очистку
*/
void ntsOnPlaybackDialogTerminated(long id, String deviceId);
}
/**
* Часть интерфейса JNI, rtp ps C++ реализация упаковки и отправки кода
*/
public class SmartPublisherJniV2 {
/**
* Open издатель (запуск экземпляра push)
*
* @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 (прямая трансляция).
* Уведомление: Настройка типа потока в настоящее время действительна только для медиапотока GB28181.
* @return {0} if successful
*/
public native int SetStreamType(long handle, int type);
/**
* Видео доставки on пакет спроса, В настоящее время используется только для нажатия GB28181, Обратите внимание, что объект ByteBuffer должен быть DirectBuffer.
*
* @param codec_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: Необязательный параметр, можно передать ноль, Для пакета ключевых кадров H264: Если пакет не содержит sps и pps, Может передать 0x00000001 sps 0x00000001 pps
* , для пакета ключевых кадров H265, Если в пакете нет 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);
/**
* Опубликовать аудио вкл. пакет спроса, В настоящее время используется только для нажатия GB28181, Обратите внимание, что объект ByteBuffer должен быть DirectBuffer.
*
* @param codec_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 После того, как источник завершит поиск, пожалуйста, позвони
* @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);
/**
* Закройте экземпляр push. В конце необходимо вызвать интерфейс закрытия для освобождения ресурсов.
*
* @return {0} if successful
*/
public native int SmartPublisherClose(long handle);
}
/**
* Код реализации части прослушивателя
*/
public class PlaybackListenerImpl implements com.gb.ntsignalling.GBSIPAgentPlaybackListener {
/*
*Получено приглашение на загрузку файла с помощью s=Playback.
*/
@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);
}
}
/*
*Отправить воспроизведение 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();
}
/*
* ОТМЕНА получено Playback ПРИГЛАСИТЕ запрос
*/
@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();
}
/*
* Подтверждение получено
*/
@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();
}
/*
* Получено до свидания
*/
@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();
}
/*
* Не получаю ПОКА В случае сообщения Прекратить воспроизведение
*/
@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();
}
/*
* Разговор, соответствующий сеансу воспроизведения, завершается, Обычно этот обратный вызов не запускается. В настоящее время он отвечает только на 200 КБ. Но если вы не получили ACK по истечении времени 64*T1, вы можете начинать.
получил это, Пожалуйста, сделайте соответствующую очистку
*/
@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();
}
}
Платформа Android GB28181 воспроизводит исторические видео и аудиофайлы, помимо вышеуказанного взаимодействия сигнализации, также необходимо обрабатывать упаковку и отправку RTP и т. д. По сравнению с другими функциями реализация более сложна. Заинтересованные разработчики могут попробовать.