
Одна из моих IP камер перестала сохранять настройки для FTP сервера и перестала в него писать. В остальном она работает, поток RTSP смотреть можно. Камера писала в формате DAV, а роутер будет писать как получится.
Роутер тот самый, что пишет онлайн радио в себя и делает другие безобразия. Надо бы ему, наверно, уже имя дать.
А получится так: rtsp из камеры, в бесплатное облако, из облака m4s куски в роутер и потом ffmpeg склеит их в mp4 на компе.
Бесплатным облаком будет rtsp.me, правда, у меня не получилось нормально качать HD разрешение (2048×1536). Их сервер начинает слать 6 секундные куски раз в 30-40 секунд, ну что это такое. А может и камера виновата. Всё равно будем качать, что есть, доп. поток 704х576. Чтоб два раза не вставать запишем и IPTV какой нибудь, там скрипт ещё проще и тянет 1920×1080.
Первым делом нужна ссылка на вашу трансляцию в облаке, такого формата https://rtsp.me/embed/kB0DezA0/
Из страницы по этой ссылке мы парсим ссылку на… плейлист похоже, такого вида https://spb.rtsp.me/twZbVylpWvRB88822cHJuA/1781030350/hls/kB0DezA0.m3u8?ip=41.109.233.11
Разбираем её на основную часть, ID и хвостик с нашим IP. Разобрав ссылку, запрашиваем в цикле плейлист и качаем все части, которые ещё не скачаны. Между загрузками частей делаем небольшую паузу, между загрузками плейлистов тоже. При старте скрипта стоит пауза 10 секунд для того, чтобы сервер хотя бы на второй запрос ответил не 403, а плейлистом. В целом паузы, на мой взгляд, подобраны удачно. Я бы их не менял.
Скрипт rtspRecorder
Скрытый текст
:local rtspMeUrl "https://rtsp.me/embed/kB0DezA0/":local diskSlot "usb1":local freeSpace (([/disk print as-value where slot=$diskSlot]->0)->"free")# проверить диск и свободное место, минимально допустимое свободное место (100 MB):if (([:len "$freeSpace"] = 0) || ($freeSpace < 104857600)) do={ :log warning ("low disk space on " . $diskSlot) :quit}:local getFolderPath do={ :local sDate [/system clock get date] :local sTime [/system clock get time] :return ("usb1/cam_video/" . [:pick $sDate 0 4] . [:pick $sDate 5 7] . [:pick $sDate 8 10] . "/" . [:pick $sTime 0 2])}:local videoDateFolder [$getFolderPath]:do { :local rtspMeUrlHtml ([/tool fetch url=$rtspMeUrl output=user as-value]->"data") :local streamLinkStartMarker ".get('" :local streamLinkEndMarker "')" :local startMarkerPos [:find $rtspMeUrlHtml $streamLinkStartMarker] :if ([:len $startMarkerPos] != 0) do={ :set startMarkerPos ($startMarkerPos + [:len $streamLinkStartMarker]) :local endMarkerPos [:find $rtspMeUrlHtml $streamLinkEndMarker $startMarkerPos] :if ([:len $endMarkerPos] != 0) do={ :local hlsUrl [:pick $rtspMeUrlHtml $startMarkerPos $endMarkerPos] #:log warning "HLS_Parser SUCCESS! Found URL: $hlsUrl" # ВЫРЕЗАЕМ БАЗОВЫЙ URL, ID ПОТОКА И ПАРАМЕТРЫ :local hlsPathMarker "/hls/" :local hlsPathPos [:find $hlsUrl $hlsPathMarker] # Вырезаем базовый URL :local baseUrl [:pick $hlsUrl 0 ($hlsPathPos + [:len $hlsPathMarker])] # Вырезаем ID потока :local m3u8Pos [:find $hlsUrl ".m3u8"] :local streamId [:pick $hlsUrl ($hlsPathPos + 5) $m3u8Pos] # Вырезаем GET-параметры :local paramPos [:find $hlsUrl "?"] :local urlParams "" :if ([:len $paramPos] != 0) do={ :set urlParams [:pick $hlsUrl $paramPos [:len $hlsUrl]] } :local playlistContent :local startPosMsLink :local endPosMsLink :set playlistContent ([/tool fetch url=$hlsUrl output=user as-value]->"data") :delay 10s :local hasChunks true :local hasChunk true :local playlistDelay 2 :while ($hasChunks) do={ :set videoDateFolder [$getFolderPath] :local baseFileLocalPath ($videoDateFolder . "/" . $streamId . ".mp4") :if ([:len [/file find where name=$baseFileLocalPath]] = 0) do={ :set freeSpace (([/disk print as-value where slot=$diskSlot]->0)->"free") :if (([:len "$freeSpace"] = 0) || ($freeSpace < 104857600)) do={ :log warning ("low disk space on " . $diskSlot) :quit } :delay 1s /tool fetch url=($baseUrl . $streamId . ".mp4") dst-path=$baseFileLocalPath keep-result=yes } :delay $playlistDelay :set playlistContent ([/tool fetch url=$hlsUrl output=user as-value]->"data") #:log warning ("playlistContent : " . $playlistContent) :local searchPosPointer 0 :local startPosMsLink [:find $playlistContent ($streamId . "-") $searchPosPointer] :if ([:len $startPosMsLink] = 0) do={ :log info ("No Links in List") :set hasChunks false :set hasChunk false } else={ :set hasChunk true } :local downloadedCount 0 :while ($hasChunk) do={ :local endPosMsLink [:find $playlistContent "\n" $startPosMsLink] :local segmentFile [:pick $playlistContent $startPosMsLink $endPosMsLink] #:log info ("Found chunk: " . $segmentFile) :local chunkFileLocalPath ($videoDateFolder . "/" . $segmentFile) :if ([:len [/file find where name=$chunkFileLocalPath]] = 0) do={ :local chunkFileUrl ($baseUrl . $segmentFile) :log warning ("Downloading: " . $chunkFileUrl) :do { :set freeSpace (([/disk print as-value where slot=$diskSlot]->0)->"free") :if (([:len "$freeSpace"] = 0) || ($freeSpace < 104857600)) do={ :log warning ("low disk space on " . $diskSlot) :quit } :delay 1 /tool fetch url=$chunkFileUrl dst-path=$chunkFileLocalPath keep-result=yes :set downloadedCount ($downloadedCount + 1) } on-error={ :log warning ("Failed to download chunk: " . $chunkFileUrl) } } :set searchPosPointer ($endPosMsLink + 1) :set startPosMsLink [:find $playlistContent ($streamId . "-") $searchPosPointer] :if ([:len $startPosMsLink] = 0) do={ :log info ("End Links") :set hasChunk false } } :if ($hasChunks) do={ :if ($downloadedCount > 0) do={ :local calculatedDelay (10 - (2 * $downloadedCount)) :if ($calculatedDelay < 2) do={ :set calculatedDelay 2 } :set playlistDelay $calculatedDelay :log info "HLS_Parser: Downloaded $downloadedCount chunks. Next playlist check in $playlistDelay s." } else={ :set playlistDelay 10 :log info "HLS_Parser: No NEW chunks in playlist. Cooling down for 6s." } } } :log info "HLS_Parser: All segments from current playlist verified." } else={ :log error "HLS_Parser: End marker not found." } } else={ :log error "HLS_Parser: Start marker not found on page." }} on-error={ :log error ("HLS_Parser ERROR: ")}
По традиции скрипт в планировщике запускаем с проверкой. Каждые 15 секунд
:if ([:len [/system script job find script=rtspRecorder]] = 0) do={ /system script run rtspRecorder} else={ :log warning "Скрипт rtspRecorder уже работает"}
Он пишет на диск usb1, в папку usb1/cam_video/ГодМесяцДень/Час , пишет непрерывно, пока есть интернет и память на диске. Для того чтобы посмотреть видео, качаем из роутера папку с нужным часом, кладём в эту же папку файл ffmpeg.exe. Открываем командную строку прямо в этой папке. Введите в адресной строке проводника cmd и нажмите Enter И вводим в консоль две команды. kB0DezA0 в командах, это ID потока, у вас он будет другой.
copy /b kB0DezA0.mp4 + kB0DezA0-*.m4s merged_raw.mp4ffmpeg -i merged_raw.mp4 -c copy ready_video.mp4
Файл ready_video.mp4 надо смотреть, остальное можно удалить.
С IPTV дело обстоит ещё проще, я попробовал качать SD и HD канал, качаются оба. Вот есть какой то канал
#EXTINF:-1 tvg-id="360.ru@SD",360° (1080p)https://cdn-evacoder-tv.facecast.io/evacoder_hls_hi/CkxfR1xNUAJwTgtXTBZTAJli/index.m3u8
Он отвечает в таком формате
#EXTM3U#EXT-X-MEDIA-SEQUENCE:5485341#EXT-X-TARGETDURATION:11#EXTINF:10,0_5485236.ts#EXTINF:10,0_5485237.ts#EXTINF:10,0_5485238.ts#EXTINF:10,0_5485239.ts#EXTINF:10,0_5485240.ts
И скрипт для него
Скрипт hdIptvRecorder
Скрытый текст
:local iptvBaseUrl "https://cdn-evacoder-tv.facecast.io/evacoder_hls_hi/CkxfR1xNUAJwTgtXTBZTAJli/":local iptvListUrl ($iptvBaseUrl . "0.m3u8"):local diskSlot "usb1":local freeSpace (([/disk print as-value where slot=$diskSlot]->0)->"free")# проверить диск и свободное место, минимально допустимое свободное место (100 MB):if (([:len "$freeSpace"] = 0) || ($freeSpace < 104857600)) do={ :log warning ("low disk space on " . $diskSlot) :quit}:local getFolderPath do={ :local sDate [/system clock get date] :local sTime [/system clock get time] :return ("usb1/hd_iptv_channel/" . [:pick $sDate 0 4] . [:pick $sDate 5 7] . [:pick $sDate 8 10] . "/" . [:pick $sTime 0 2])}:local videoDateFolder [$getFolderPath]:do { :local playlistContent :local startPosTsLink :local endPosTsLink :local hasChunks true :local hasChunk true :local playlistDelay 1 :while ($hasChunks) do={ :delay $playlistDelay :set playlistContent ([/tool fetch url=$iptvListUrl output=user as-value]->"data") #:log warning ("playlistContent : " . $playlistContent) :local searchPosPointer 0 :local startPosTsLink [:find $playlistContent ("0_") $searchPosPointer] :if ([:len $startPosTsLink] = 0) do={ :log info ("No Links in List") :set hasChunks false :set hasChunk false } else={ :set hasChunk true } :local downloadedCount 0 :while ($hasChunk) do={ :local endPosTsLink [:find $playlistContent "\n" $startPosTsLink] :local segmentFile [:pick $playlistContent $startPosTsLink $endPosTsLink] #:log info ("Found chunk: " . $segmentFile) :set videoDateFolder [$getFolderPath] :local chunkFileLocalPath ($videoDateFolder . "/" . $segmentFile) :if ([:len [/file find where name=$chunkFileLocalPath]] = 0) do={ :set freeSpace (([/disk print as-value where slot=$diskSlot]->0)->"free") :if (([:len "$freeSpace"] = 0) || ($freeSpace < 104857600)) do={ :log warning ("low disk space on " . $diskSlot) :quit } :local chunkFileUrl ($iptvBaseUrl . $segmentFile) :log warning ("Downloading: " . $chunkFileUrl) :do { :delay 1 /tool fetch url=$chunkFileUrl dst-path=$chunkFileLocalPath keep-result=yes :set downloadedCount ($downloadedCount + 1) } on-error={ :log warning ("Failed to download chunk: " . $chunkFileUrl) } } :set searchPosPointer ($endPosTsLink + 1) :set startPosTsLink [:find $playlistContent ("0_") $searchPosPointer] :if ([:len $startPosTsLink] = 0) do={ :log info ("End Links") :set hasChunk false } } :if ($hasChunks) do={ :if ($downloadedCount > 0) do={ :local calculatedDelay (10 - (2 * $downloadedCount)) :if ($calculatedDelay < 2) do={ :set calculatedDelay 1 } :set playlistDelay $calculatedDelay :log info "HLS_Parser: Downloaded $downloadedCount chunks. Next playlist check in $playlistDelay s." } else={ :set playlistDelay 10 :log info "HLS_Parser: No NEW chunks in playlist. Cooling down for 6s." } } } :log info "HLS_Parser: All segments from current playlist verified."} on-error={ :log error ("HLS_Parser ERROR: ")}
Скрипт запускается так же в планировщике, раз в 15 секунд с проверкой. Качает так же, в папки с датой и часом. Склеивается ещё проще, командами
copy /b *.ts merged.tsffmpeg -i merged.ts -c copy ready_video.mp4
И так же смотрим ready_video.mp4 , а остальное удаляем после склеивания.
Для других каналов, которые отвечают в таком же формате, например канал
#EXTINF:-1 tvg-id="",Gaki no Tsukai (English Subs) (720p)https://hamada.gaki-no-tsukai.stream/hls/test.m3u8
с ответом
#EXTM3U#EXT-X-VERSION:3#EXT-X-MEDIA-SEQUENCE:2479#EXT-X-TARGETDURATION:11#EXTINF:3.567,test-2479.ts#EXTINF:6.033,test-2480.ts#EXTINF:8.334,test-2481.ts#EXTINF:3.333,test-2482.ts
в скрипте надо будет поменять только ссылку, разрезав её на базовую часть и хвостик.
И поменять в 2х местах маркер поиска начала ссылки на часть, то есть искать не 0_ , а test- и всё.
Если не совсем понятно, то вот так
Скрытый текст
:local iptvBaseUrl "https://hamada.gaki-no-tsukai.stream/hls/":local iptvListUrl ($iptvBaseUrl . "test.m3u8"):local diskSlot "usb1":local freeSpace (([/disk print as-value where slot=$diskSlot]->0)->"free")# проверить диск и свободное место, минимально допустимое свободное место (100 MB):if (([:len "$freeSpace"] = 0) || ($freeSpace < 104857600)) do={ :log warning ("low disk space on " . $diskSlot) :quit}:local getFolderPath do={ :local sDate [/system clock get date] :local sTime [/system clock get time] :return ("usb1/iptv_channel/" . [:pick $sDate 0 4] . [:pick $sDate 5 7] . [:pick $sDate 8 10] . "/" . [:pick $sTime 0 2])}:local videoDateFolder [$getFolderPath]:do { :local playlistContent :local startPosTsLink :local endPosTsLink :local hasChunks true :local hasChunk true :local playlistDelay 1 :while ($hasChunks) do={ :delay $playlistDelay :set playlistContent ([/tool fetch url=$iptvListUrl output=user as-value]->"data") #:log warning ("playlistContent : " . $playlistContent) :local searchPosPointer 0 :local startPosTsLink [:find $playlistContent ("test-") $searchPosPointer] :if ([:len $startPosTsLink] = 0) do={ :log info ("No Links in List") :set hasChunks false :set hasChunk false } else={ :set hasChunk true } :local downloadedCount 0 :while ($hasChunk) do={ :local endPosTsLink [:find $playlistContent "\n" $startPosTsLink] :local segmentFile [:pick $playlistContent $startPosTsLink $endPosTsLink] #:log info ("Found chunk: " . $segmentFile) :set videoDateFolder [$getFolderPath] :local chunkFileLocalPath ($videoDateFolder . "/" . $segmentFile) :if ([:len [/file find where name=$chunkFileLocalPath]] = 0) do={ :set freeSpace (([/disk print as-value where slot=$diskSlot]->0)->"free") :if (([:len "$freeSpace"] = 0) || ($freeSpace < 104857600)) do={ :log warning ("low disk space on " . $diskSlot) :quit } :local chunkFileUrl ($iptvBaseUrl . $segmentFile) :log warning ("Downloading: " . $chunkFileUrl) :do { :delay 1 /tool fetch url=$chunkFileUrl dst-path=$chunkFileLocalPath keep-result=yes :set downloadedCount ($downloadedCount + 1) } on-error={ :log warning ("Failed to download chunk: " . $chunkFileUrl) } } :set searchPosPointer ($endPosTsLink + 1) :set startPosTsLink [:find $playlistContent ("test-") $searchPosPointer] :if ([:len $startPosTsLink] = 0) do={ :log info ("End Links") :set hasChunk false } } :if ($hasChunks) do={ :if ($downloadedCount > 0) do={ :local calculatedDelay (10 - (2 * $downloadedCount)) :if ($calculatedDelay < 2) do={ :set calculatedDelay 1 } :set playlistDelay $calculatedDelay :log info "HLS_Parser: Downloaded $downloadedCount chunks. Next playlist check in $playlistDelay s." } else={ :set playlistDelay 10 :log info "HLS_Parser: No NEW chunks in playlist. Cooling down for 6s." } } } :log info "HLS_Parser: All segments from current playlist verified."} on-error={ :log error ("HLS_Parser ERROR: ")}
ссылка на оригинал статьи https://habr.com/ru/articles/1045708/