diff --git a/conf/config.ini b/conf/config.ini index ae0ce077..fda77b7b 100644 --- a/conf/config.ini +++ b/conf/config.ini @@ -94,6 +94,7 @@ maxStreamWaitMS=15000 #某个流无人观看时,触发hook.on_stream_none_reader事件的最大等待时间,单位毫秒 #在配合hook.on_stream_none_reader事件时,可以做到无人观看自动停止拉流或停止接收推流 streamNoneReaderDelayMS=20000 +noRecordStreamNoneReaderDelayMS=130000 #拉流代理时如果断流再重连成功是否删除前一次的媒体流数据,如果删除将重新开始, #如果不删除将会接着上一次的数据继续写(录制hls/mp4时会继续在前一个文件后面写) resetWhenRePlay=1 diff --git a/src/Common/MediaSource.cpp b/src/Common/MediaSource.cpp index 5e378124..3bfe6eac 100644 --- a/src/Common/MediaSource.cpp +++ b/src/Common/MediaSource.cpp @@ -662,32 +662,73 @@ void MediaSourceEvent::onReaderChanged(MediaSource &sender, int size){ //没有任何人观看该视频源,表明该源可以关闭了 GET_CONFIG(string, record_app, Record::kAppName); GET_CONFIG(int, stream_none_reader_delay, General::kStreamNoneReaderDelayMS); + GET_CONFIG(int, no_record_stream_none_reader_delay, General::kNoRecordStreamNoneReaderDelayMS); //如果mp4点播, 无人观看时我们强制关闭点播 bool is_mp4_vod = sender.getApp() == record_app; weak_ptr weak_sender = sender.shared_from_this(); - _async_close_timer = std::make_shared(stream_none_reader_delay / 1000.0f, [weak_sender, is_mp4_vod]() { - auto strong_sender = weak_sender.lock(); - if (!strong_sender) { - //对象已经销毁 - return false; - } + if(sender.isRecording(Recorder::type_hls)) {//如果正在录像 + WarnL << "************The stream is Recording.*************"; + WarnL << sender.getUrl(); + _async_close_timer = std::make_shared( + stream_none_reader_delay / 1000.0f, + [weak_sender, is_mp4_vod]() { + auto strong_sender = weak_sender.lock(); + if (!strong_sender) { + //对象已经销毁 + return false; + } - if (strong_sender->totalReaderCount()) { - //还有人观看该视频,不触发关闭事件 - return false; - } + if (strong_sender->totalReaderCount()) { + //还有人观看该视频,不触发关闭事件 + return false; + } - if (!is_mp4_vod) { - //直播时触发无人观看事件,让开发者自行选择是否关闭 - NoticeCenter::Instance().emitEvent(Broadcast::kBroadcastStreamNoneReader, *strong_sender); - } else { - //这个是mp4点播,我们自动关闭 - WarnL << "MP4点播无人观看,自动关闭:" << strong_sender->getUrl(); - strong_sender->close(false); - } - return false; - }, nullptr); + if (!is_mp4_vod) { + //直播时触发无人观看事件,让开发者自行选择是否关闭 + NoticeCenter::Instance().emitEvent(Broadcast::kBroadcastStreamNoneReader, *strong_sender); + } else { + //这个是mp4点播,我们自动关闭 + WarnL << "MP4点播无人观看,自动关闭:" << strong_sender->getUrl(); + strong_sender->close(false); + } + return false; + }, + nullptr); + } else {//没有录像的话 + WarnL << "************The stream is Not Recording.*************"; + WarnL << sender.getUrl(); + _async_close_timer = std::make_shared( + no_record_stream_none_reader_delay / 1000.0f, + [weak_sender, is_mp4_vod]() { + auto strong_sender = weak_sender.lock(); + + if (strong_sender->isRecording(Recorder::type_hls)) { + return false; + } + if (!strong_sender) { + //对象已经销毁 + return false; + } + + if (strong_sender->totalReaderCount()) { + //还有人观看该视频,不触发关闭事件 + return false; + } + + if (!is_mp4_vod) { + //直播时触发无人观看事件,让开发者自行选择是否关闭 + NoticeCenter::Instance().emitEvent(Broadcast::kBroadcastStreamNoneReader, *strong_sender); + } else { + //这个是mp4点播,我们自动关闭 + WarnL << "MP4点播无人观看,自动关闭:" << strong_sender->getUrl(); + strong_sender->close(false); + } + return false; + }, + nullptr); + } + } string MediaSourceEvent::getOriginUrl(MediaSource &sender) const { diff --git a/src/Common/MultiMediaSourceMuxer.cpp b/src/Common/MultiMediaSourceMuxer.cpp index bcac76b5..d6a1d2f8 100644 --- a/src/Common/MultiMediaSourceMuxer.cpp +++ b/src/Common/MultiMediaSourceMuxer.cpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2016 The ZLMediaKit project authors. All Rights Reserved. * * This file is part of ZLMediaKit(https://github.com/xia-chu/ZLMediaKit). @@ -192,8 +192,9 @@ bool MultiMediaSourceMuxer::setupRecord(MediaSource &sender, Recorder::type type }); switch (type) { case Recorder::type_hls : { - if (start && !_hls) { - //开始录制 + if (!_hls) + { + //创建hls对象 _option.hls_save_path = custom_path; auto hls = dynamic_pointer_cast(makeRecorder(sender, getTracks(), type, _option)); if (hls) { @@ -201,10 +202,8 @@ bool MultiMediaSourceMuxer::setupRecord(MediaSource &sender, Recorder::type type hls->setListener(shared_from_this()); } _hls = hls; - } else if (!start && _hls) { - //停止录制 - _hls = nullptr; } + _hls->startRecord(start); return true; } case Recorder::type_mp4 : { @@ -227,7 +226,12 @@ bool MultiMediaSourceMuxer::setupRecord(MediaSource &sender, Recorder::type type bool MultiMediaSourceMuxer::isRecording(MediaSource &sender, Recorder::type type) { switch (type){ case Recorder::type_hls : - return !!_hls; + //return !!_hls; + if (_hls){ + return _hls->getRecordFlag(); + }else{ + return false; + } case Recorder::type_mp4 : return !!_mp4; default: diff --git a/src/Common/config.cpp b/src/Common/config.cpp index cb24064b..30ba5cca 100644 --- a/src/Common/config.cpp +++ b/src/Common/config.cpp @@ -66,6 +66,7 @@ namespace General { const string kMediaServerId = GENERAL_FIELD "mediaServerId"; const string kFlowThreshold = GENERAL_FIELD "flowThreshold"; const string kStreamNoneReaderDelayMS = GENERAL_FIELD "streamNoneReaderDelayMS"; +const string kNoRecordStreamNoneReaderDelayMS = GENERAL_FIELD "noRecordStreamNoneReaderDelayMS"; const string kMaxStreamWaitTimeMS = GENERAL_FIELD "maxStreamWaitMS"; const string kEnableVhost = GENERAL_FIELD "enableVhost"; const string kResetWhenRePlay = GENERAL_FIELD "resetWhenRePlay"; @@ -79,6 +80,7 @@ const string kUnreadyFrameCache = GENERAL_FIELD "unready_frame_cache"; static onceToken token([]() { mINI::Instance()[kFlowThreshold] = 1024; mINI::Instance()[kStreamNoneReaderDelayMS] = 20 * 1000; + mINI::Instance()[kNoRecordStreamNoneReaderDelayMS] = 130 * 1000; mINI::Instance()[kMaxStreamWaitTimeMS] = 15 * 1000; mINI::Instance()[kEnableVhost] = 0; mINI::Instance()[kResetWhenRePlay] = 1; diff --git a/src/Common/config.h b/src/Common/config.h index 36177ec4..260178fd 100644 --- a/src/Common/config.h +++ b/src/Common/config.h @@ -158,6 +158,7 @@ extern const std::string kFlowThreshold; // 流无人观看并且超过若干时间后才触发kBroadcastStreamNoneReader事件 // 默认连续5秒无人观看然后触发kBroadcastStreamNoneReader事件 extern const std::string kStreamNoneReaderDelayMS; +extern const std::string kNoRecordStreamNoneReaderDelayMS; // 等待流注册超时时间,收到播放器后请求后,如果未找到相关流,服务器会等待一定时间, // 如果在这个时间内,相关流注册上了,那么服务器会立即响应播放器播放成功, // 否则会最多等待kMaxStreamWaitTimeMS毫秒,然后响应播放器播放失败 diff --git a/src/Record/HlsMakerImpSub.cpp b/src/Record/HlsMakerImpSub.cpp new file mode 100644 index 00000000..3842ca30 --- /dev/null +++ b/src/Record/HlsMakerImpSub.cpp @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2016 The ZLMediaKit project authors. All Rights Reserved. + * + * This file is part of ZLMediaKit(https://github.com/xia-chu/ZLMediaKit). + * + * Use of this source code is governed by MIT license that can be found in the + * LICENSE file in the root of the source tree. All contributing project authors + * may be found in the AUTHORS file in the root of the source tree. + */ + +#include +#include +#include "HlsMakerImpSub.h" +#include "Util/util.h" +#include "Util/uv_errno.h" + +using namespace std; +using namespace toolkit; + +namespace mediakit { + +HlsMakerImpSub::HlsMakerImpSub( + const string &m3u8_file, + const string ¶ms, + uint32_t bufSize, + float seg_duration, + uint32_t seg_number, + bool seg_keep):HlsMakerSub(seg_duration, seg_number, seg_keep) { + _path_prefix = m3u8_file.substr(0, m3u8_file.rfind('/')); + _path_hls = m3u8_file; + _params = params; + _buf_size = bufSize; + _file_buf.reset(new char[bufSize], [](char *ptr) { + delete[] ptr; + }); + + _info.folder = _path_prefix; +} + +HlsMakerImpSub::~HlsMakerImpSub() { + clearCache(false, true); +} + +void HlsMakerImpSub::clearCache() { + clearCache(true, false); +} + +void HlsMakerImpSub::clearCache(bool immediately, bool eof) { + //录制完了 + flushLastSegment(eof); + if (!isLive()||isKeep()) { + return; + } + + clear(); + _file = nullptr; + //删除_segment_file_paths路径对应的直播文件 + for (auto it : _segment_file_paths) { + auto ts_path = it.second; + File::delete_file(ts_path.data()); + } + _segment_file_paths.clear(); + + //程序异常退出的情况下,直播的8个ts文件还是无法删除, + //这里先删除掉m3u8文件对应的3个ts文件。还有保留的5个ts文件无法删除,能删除几个是几个吧。 + //fstream file(_path_prefix + "/hls.m3u8"); + //string data; + //while (getline(file,data)) { + // string ts_path = _path_prefix + "/" + data; + // File::delete_file(ts_path.data()); + //} + //file.close(); + + //删除缓存的m3u8文件 + File::delete_file((_path_prefix + "/hls.m3u8").data()); +} + +string HlsMakerImpSub::onOpenSegment(uint64_t index) { + string segment_name, segment_path; + + auto strDate = getTimeStr("%Y-%m-%d"); + auto strHour = getTimeStr("%H"); + auto strTime = getTimeStr("%M-%S"); + segment_name = StrPrinter << strDate + "_" + strHour + "-" + strTime << ".ts"; + segment_path = _path_prefix + "/" + strDate + "/" + strHour + "/" + segment_name; + if ((!isKeep())) { + _segment_file_paths.emplace(index, segment_path); + } + + _file = makeFile(segment_path, true); + + //保存本切片的元数据 + _info.start_time = ::time(NULL); + _info.file_name = segment_name; + _info.file_path = segment_path; + _info.url = _info.app + "/" + _info.stream + "/" + segment_name; + + if (!_file) { + WarnL << "create file failed," << segment_path << " " << get_uv_errmsg(); + return ""; + } + if (_params.empty()) { + return strDate + "/" + strHour + "/" + segment_name; + } + return strDate + "/" + strHour + "/" + segment_name + "?" + _params; +} + +void HlsMakerImpSub::onDelSegment(uint64_t index) { + auto it = _segment_file_paths.find(index); + if (it == _segment_file_paths.end()) { + return; + } + File::delete_file(it->second.data()); + _segment_file_paths.erase(it); +} + +void HlsMakerImpSub::onWriteSegment(const char *data, size_t len) { + if (_file) { + fwrite(data, len, 1, _file.get()); + } + if (_media_src) { + _media_src->onSegmentSize(len); + } +} + +void HlsMakerImpSub::onWriteHls(const std::string &data) { + auto hls = makeFile(_path_hls); + if (hls) { + fwrite(data.data(), data.size(), 1, hls.get()); + hls.reset(); + if (_media_src) { + _media_src->setIndexFile(data); + } + } else { + WarnL << "create hls file failed," << _path_hls << " " << get_uv_errmsg(); + } + //DebugL << "\r\n" << string(data,len); +} + +void HlsMakerImpSub::onFlushLastSegment(uint64_t duration_ms) { + //关闭并flush文件到磁盘 + _file = nullptr; + + GET_CONFIG(bool, broadcastRecordTs, Hls::kBroadcastRecordTs); + if (broadcastRecordTs) { + _info.time_len = duration_ms / 1000.0f; + _info.file_size = File::fileSize(_info.file_path.data()); + NoticeCenter::Instance().emitEvent(Broadcast::kBroadcastRecordTs, _info); + } +} + +std::shared_ptr HlsMakerImpSub::makeFile(const string &file, bool setbuf) { + auto file_buf = _file_buf; + auto ret = shared_ptr(File::create_file(file.data(), "wb"), [file_buf](FILE *fp) { + if (fp) { + fclose(fp); + } + }); + if (ret && setbuf) { + setvbuf(ret.get(), _file_buf.get(), _IOFBF, _buf_size); + } + return ret; +} + +void HlsMakerImpSub::setMediaSource(const string &vhost, const string &app, const string &stream_id) { + _media_src = std::make_shared(vhost, app, stream_id); + _info.app = app; + _info.stream = stream_id; + _info.vhost = vhost; +} + +HlsMediaSource::Ptr HlsMakerImpSub::getMediaSource() const { + return _media_src; +} + +std::string HlsMakerImpSub::getPathPrefix() { + return _path_prefix; +} + +}//namespace mediakit \ No newline at end of file diff --git a/src/Record/HlsMakerImpSub.h b/src/Record/HlsMakerImpSub.h new file mode 100644 index 00000000..043937b7 --- /dev/null +++ b/src/Record/HlsMakerImpSub.h @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016 The ZLMediaKit project authors. All Rights Reserved. + * + * This file is part of ZLMediaKit(https://github.com/xia-chu/ZLMediaKit). + * + * Use of this source code is governed by MIT license that can be found in the + * LICENSE file in the root of the source tree. All contributing project authors + * may be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef HLSMAKERIMPSUB_H +#define HLSMAKERIMPSUB_H + +#include +#include +#include +#include "HlsMakerSub.h" +#include "HlsMediaSource.h" + +namespace mediakit { + +class HlsMakerImpSub : public HlsMakerSub{ +public: + HlsMakerImpSub( + const std::string &m3u8_file, + const std::string ¶ms, + uint32_t bufSize = 64 * 1024, + float seg_duration = 5, + uint32_t seg_number = 3, + bool seg_keep = false); + + ~HlsMakerImpSub() override; + + /** + * 设置媒体信息 + * @param vhost 虚拟主机 + * @param app 应用名 + * @param stream_id 流id + */ + void setMediaSource(const std::string &vhost, const std::string &app, const std::string &stream_id); + + /** + * 获取MediaSource + * @return + */ + HlsMediaSource::Ptr getMediaSource() const; + + /** + * 清空缓存 + */ + void clearCache(); + +protected: + std::string onOpenSegment(uint64_t index) override ; + void onDelSegment(uint64_t index) override; + void onWriteSegment(const char *data, size_t len) override; + void onWriteHls(const std::string &data) override; + void onFlushLastSegment(uint64_t duration_ms) override; + std::string getPathPrefix() override; + +private: + std::shared_ptr makeFile(const std::string &file,bool setbuf = false); + void clearCache(bool immediately, bool eof); + +private: + int _buf_size; + std::string _params; + std::string _path_hls; + std::string _path_prefix; + RecordInfo _info; + std::shared_ptr _file; + std::shared_ptr _file_buf; + HlsMediaSource::Ptr _media_src; + +}; + +}//namespace mediakit +#endif //HLSMAKERIMPSUB_H diff --git a/src/Record/HlsMakerSub.cpp b/src/Record/HlsMakerSub.cpp new file mode 100644 index 00000000..84a033ae --- /dev/null +++ b/src/Record/HlsMakerSub.cpp @@ -0,0 +1,327 @@ +/* + * Copyright (c) 2016 The ZLMediaKit project authors. All Rights Reserved. + * + * This file is part of ZLMediaKit(https://github.com/xia-chu/ZLMediaKit). + * + * Use of this source code is governed by MIT license that can be found in the + * LICENSE file in the root of the source tree. All contributing project authors + * may be found in the AUTHORS file in the root of the source tree. + */ + + #include "Util/File.h" +#include "HlsMakerSub.h" +#if defined(_WIN32) +#include +#define _access access +#else +#include +#include +#endif // WIN32 + + +using namespace std; +using namespace toolkit; + +namespace mediakit { + +HlsMakerSub::HlsMakerSub(float seg_duration, uint32_t seg_number, bool seg_keep) { + //最小允许设置为0,0个切片代表点播 + _seg_number = seg_number; + _seg_duration = seg_duration; + _seg_keep = seg_keep; + _is_record = false; + //_poller = EventPollerPool::Instance().getPoller(); +} + +HlsMakerSub::~HlsMakerSub() { + + _is_close_stream = true; +} + +void HlsMakerSub::startRecord(bool isRecord) { + //本来已经在录像,再次点击录像,或者本来已经停止录像,再次点击停止录像,直接返回 + if (isRecord == _is_record) { + return; + } + ////如果是录像,则删除之前直播的8个ts文件 + //if (isRecord) { + // std::map delete_file_paths = _segment_file_paths; + // _segment_file_paths.clear(); + // int count = 0; + // //删除_segment_file_paths路径对应的直播文件,过10s再删除,免得hls直播突然断掉 + // for (auto it : delete_file_paths) { + // count ++; + // if (count < delete_file_paths.size()) { + // auto ts_path = it.second; + // File::delete_file(ts_path.data()); + // _poller->doDelayTask(10 * 1000, [ts_path]() { + // File::delete_file(ts_path.data()); + // return 0; + // }); + // } + // } + //} + + if(isRecord) { + _seg_keep = true; + }else{ + _seg_keep = false; + _is_close_stream = true; + } + _is_record = isRecord; +} + + +void HlsMakerSub::makeIndexFile(bool eof) { + char file_content[1024]; + int maxSegmentDuration = 0; + + for (auto &tp : _seg_dur_list) { + int dur = std::get<0>(tp); + if (dur > maxSegmentDuration) { + maxSegmentDuration = dur; + } + } + + auto sequence = _seg_number ? (_file_index > _seg_number ? _file_index - _seg_number : 0LL) : 0LL; + + string m3u8; + if (_seg_number == 0) { + // 录像点播支持时移 + snprintf(file_content, sizeof(file_content), + "#EXTM3U\n" + "#EXT-X-PLAYLIST-TYPE:EVENT\n" + "#EXT-X-VERSION:4\n" + "#EXT-X-TARGETDURATION:%u\n" + "#EXT-X-MEDIA-SEQUENCE:%llu\n", + (maxSegmentDuration + 999) / 1000, + sequence); + } else { + snprintf(file_content, sizeof(file_content), + "#EXTM3U\n" + "#EXT-X-VERSION:3\n" + "#EXT-X-ALLOW-CACHE:NO\n" + "#EXT-X-TARGETDURATION:%u\n" + "#EXT-X-MEDIA-SEQUENCE:%llu\n", + (maxSegmentDuration + 999) / 1000, + sequence); + } + + m3u8.assign(file_content); + + for (auto &tp : _seg_dur_list) { + snprintf(file_content, sizeof(file_content), "#EXTINF:%.3f,\n%s\n", std::get<0>(tp) / 1000.0, std::get<1>(tp).data()); + m3u8.append(file_content); + } + + if (eof) { + snprintf(file_content, sizeof(file_content), "#EXT-X-ENDLIST\n"); + m3u8.append(file_content); + } + onWriteHls(m3u8); +} + + +void HlsMakerSub::inputData(void *data, size_t len, uint64_t timestamp, bool is_idr_fast_packet) { + if (data && len) { + if (timestamp < _last_timestamp) { + //时间戳回退了,切片时长重新计时 + WarnL << "stamp reduce: " << _last_timestamp << " -> " << timestamp; + _last_seg_timestamp = _last_timestamp = timestamp; + } + if (is_idr_fast_packet) { + //尝试切片ts + addNewSegment(timestamp); + } + if (!_last_file_name.empty()) { + //存在切片才写入ts数据 + onWriteSegment((char *) data, len); + _last_timestamp = timestamp; + } + } else { + //resetTracks时触发此逻辑 + flushLastSegment(false); + } +} + +void HlsMakerSub::delOldSegment() { + if (_seg_number == 0) { + //如果设置为保留0个切片,则认为是保存为点播 + return; + } + //在hls m3u8索引文件中,我们保存的切片个数跟_seg_number相关设置一致 + if (_file_index > _seg_number) { + _seg_dur_list.pop_front(); + } + //如果设置为一直保存,就不删除 + if (_seg_keep) { + return; + } + GET_CONFIG(uint32_t, segRetain, Hls::kSegmentRetain); + //但是实际保存的切片个数比m3u8所述多若干个,这样做的目的是防止播放器在切片删除前能下载完毕 + if (_file_index > _seg_number + segRetain) { + onDelSegment(_file_index - _seg_number - segRetain - 1); + } +} + +void HlsMakerSub::addNewSegment(uint64_t stamp) { + if (!_last_file_name.empty() && stamp - _last_seg_timestamp < _seg_duration * 1000) { + //存在上个切片,并且未到分片时间 + return; + } + + //关闭并保存上一个切片,如果_seg_number==0,那么是点播。 + flushLastSegment(false); + + //新增切片 + _last_file_name = onOpenSegment(_file_index++); + //记录本次切片的起始时间戳 + _last_seg_timestamp = _last_timestamp ? _last_timestamp : stamp; + +} + +void HlsMakerSub::flushLastSegment(bool eof) { + if (_last_file_name.empty()) { + //不存在上个切片 + return; + } + //文件创建到最后一次数据写入的时间即为切片长度 + auto seg_dur = _last_timestamp - _last_seg_timestamp; + if (seg_dur <= 0) { + seg_dur = 100; + } + _seg_dur_list.emplace_back(seg_dur, std::move(_last_file_name)); + delOldSegment(); + //先flush ts切片,否则可能存在ts文件未写入完毕就被访问的情况 + onFlushLastSegment(seg_dur); + //然后写m3u8文件 + makeIndexFile(eof); + //判断当前是否在录像,正在录像的话,生成录像的m3u8文件 + if (_is_record) { + createM3u8FileForRecord(); + } + +} + +bool HlsMakerSub::isLive() { + return _seg_number != 0; +} + +bool HlsMakerSub::isKeep() { + return _seg_keep; +} + +void HlsMakerSub::clear() { + _file_index = 0; + _last_timestamp = 0; + _last_seg_timestamp = 0; + _seg_dur_list.clear(); + _last_file_name.clear(); + +} + +std::string HlsMakerSub::getM3u8TSBody(const std::string &file_content) { + + string new_file = file_content; + if (file_content.find("#EXT-X-ENDLIST") != file_content.npos) { + //找到了,则去掉"#EXT-X-ENDLIST" + new_file = file_content.substr(0, file_content.length() - 15); + } + + string body = new_file.substr(new_file.find_last_of("#")); + //此时的body为 + //#EXTINF:4.534, + //2022-09-14/08/2022-09-14_08-35-16.ts + string extinf = body.substr(0, body.find(",")+2); + string tsFile = body.substr(body.find_last_of("/") + 1); + body.append("#EXT-X-ENDLIST\n"); + + return extinf + tsFile + "#EXT-X-ENDLIST\n"; +} + +std::string HlsMakerSub::getTsFile(const std::string &file_content) { + // 最后一个TS的body为 + // 2022-09-13/13/58-13_43.ts + // #EXT-X-ENDLIST + string body = file_content.substr(file_content.find_last_of(",") + 2); + string ts_file_name = body.substr(body.find_last_of("/") + 1); + if (ts_file_name.find("#EXT-X-ENDLIST") == ts_file_name.npos ) { + ts_file_name = ts_file_name.substr(0, ts_file_name.length() - 4); //没找到,去掉“.ts\n”,只留名字 + } else { + ts_file_name = ts_file_name.substr(0, ts_file_name.length() - 19); //找到的话,去掉“.ts\n#EXT-X-ENDLIST\n”,只留名字 + } + + return ts_file_name; +} + +void HlsMakerSub::createM3u8FileForRecord() { + // 1.读取直播目录下的m3u8文件,获取当前的ts文件以及时长,并生成m3u8文件的路径 + string live_file = File::loadFile((getPathPrefix() + "/hls.m3u8").data()); + if (live_file.empty()) { + return; + } + + string body = getM3u8TSBody(live_file); + string ts_file_name = getTsFile(live_file); // ts_file: 2022-09-14_11-06-03 + string m3u8_file = getPathPrefix() + "/" + ts_file_name.substr(0, 10) + "/" + ts_file_name.substr(11, 2) + "/"; + + // 2.判断该目录下有没有m3u8文件,没有的话,生成第一个m3u8文件,有的话,重命名 + int handle = -1; + DIR *dir_info = opendir(m3u8_file.data()); + struct dirent *dir_entry; + if (dir_info) { + while ((dir_entry =readdir(dir_info)) != NULL) { + if (end_with(dir_entry->d_name, ".m3u8")) { + handle = 0; + break; + } + } + closedir(dir_info); + } else { + return; + } + + if (-1 == handle) {//第一次播放流 + _m3u8_file_path = m3u8_file + ts_file_name + ".m3u8"; + _is_close_stream = false; + } else {//断流过,一次以上播放 + if (_is_close_stream) { + _m3u8_file_path = m3u8_file + ts_file_name + ".m3u8"; + _is_close_stream = false; + } + if (_m3u8_file_path.length() == 0) { //服务重启后,进来,_m3u8_file_path为空 + _m3u8_file_path = m3u8_file + ts_file_name + ".m3u8"; + } + } + + if (_m3u8_file_path.empty()) { + WarnL << "create m3u8 file failed, _m3u8_file_path is empty." ; + return; + } + + //3.写m3u8文件 + string m3u8Header = "#EXTM3U\n" + "#EXT-X-PLAYLIST-TYPE:EVENT\n" + "#EXT-X-VERSION:4\n" + "#EXT-X-TARGETDURATION:2\n" + "#EXT-X-MEDIA-SEQUENCE:0\n"; + + if (access(_m3u8_file_path.data(), 0) != 0) { //文件不存在 + auto file = File::create_file(_m3u8_file_path.data(), "wb"); + if (file) { + fwrite(m3u8Header.data(), m3u8Header.size(), 1, file); + fwrite(body.data(), body.size(), 1, file); + fclose(file); + } + } else { + // 第二次进来,去掉 "#EXT-X-ENDLIST\n",再重新追加file_content,保存文件 + auto file = File::create_file(_m3u8_file_path.data(), "r+"); + if (file) { + fseek(file, -15, SEEK_END); + fwrite(body.data(), body.size(), 1, file); + fclose(file); + } + } +} + +}//namespace mediakit diff --git a/src/Record/HlsMakerSub.h b/src/Record/HlsMakerSub.h new file mode 100644 index 00000000..330447c4 --- /dev/null +++ b/src/Record/HlsMakerSub.h @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2016 The ZLMediaKit project authors. All Rights Reserved. + * + * This file is part of ZLMediaKit(https://github.com/xia-chu/ZLMediaKit). + * + * Use of this source code is governed by MIT license that can be found in the + * LICENSE file in the root of the source tree. All contributing project authors + * may be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef HLSMAKERSUB_H +#define HLSMAKERSUB_H + +#include +#include +#include "Common/config.h" +#include "Util/TimeTicker.h" +#include "Util/File.h" +#include "Util/util.h" +#include "Util/logger.h" +//#include "ZLToolKit/src/Poller/EventPoller.h" +namespace mediakit { + +class HlsMakerSub { +public: + /** + * @param seg_duration 切片文件长度 + * @param seg_number 切片个数 + * @param seg_keep 是否保留切片文件 + */ + HlsMakerSub(float seg_duration = 5, uint32_t seg_number = 3, bool seg_keep = false); + virtual ~HlsMakerSub(); + + /** + * 写入ts数据 + * @param data 数据 + * @param len 数据长度 + * @param timestamp 毫秒时间戳 + * @param is_idr_fast_packet 是否为关键帧第一个包 + */ + void inputData(void *data, size_t len, uint64_t timestamp, bool is_idr_fast_packet); + + /** + * 是否为直播 + */ + bool isLive(); + + /** + * 是否保留切片文件 + */ + bool isKeep(); + + /** + * 清空记录 + */ + void clear(); + //设置是否录像标志 + void startRecord(bool isRecord); + +protected: + /** + * 创建ts切片文件回调 + * @param index + * @return + */ + virtual std::string onOpenSegment(uint64_t index) = 0; + + /** + * 删除ts切片文件回调 + * @param index + */ + virtual void onDelSegment(uint64_t index) = 0; + + /** + * 写ts切片文件回调 + * @param data + * @param len + */ + virtual void onWriteSegment(const char *data, size_t len) = 0; + + /** + * 写m3u8文件回调 + */ + virtual void onWriteHls(const std::string &data) = 0; + + /** + * 上一个 ts 切片写入完成, 可在这里进行通知处理 + * @param duration_ms 上一个 ts 切片的时长, 单位为毫秒 + */ + virtual void onFlushLastSegment(uint64_t duration_ms) {}; + virtual std::string getPathPrefix() = 0; + + /** + * 关闭上个ts切片并且写入m3u8索引 + * @param eof HLS直播是否已结束 + */ + void flushLastSegment(bool eof); + +private: + /** + * 生成m3u8文件 + * @param eof true代表点播 + */ + void makeIndexFile(bool eof = false); + + /** + * 删除旧的ts切片 + */ + void delOldSegment(); + + /** + * 添加新的ts切片 + * @param timestamp + */ + void addNewSegment(uint64_t timestamp); + + //新增函数,实现录像功能 + std::string getTsFile(const std::string &file_content); + std::string getM3u8TSBody(const std::string &file_content); + void createM3u8FileForRecord(); + +private: + float _seg_duration = 0; + uint32_t _seg_number = 0; + bool _seg_keep = false; + uint64_t _last_timestamp = 0; + uint64_t _last_seg_timestamp = 0; + uint64_t _file_index = 0; + std::string _last_file_name; + std::deque > _seg_dur_list; + bool _is_record = false; + bool _is_close_stream = false; + std::string _m3u8_file_path; + //toolkit::EventPoller::Ptr _poller; + +public: + std::map _segment_file_paths; +}; + +}//namespace mediakit +#endif //HLSMAKERSUB_H diff --git a/src/Record/HlsRecorder.h b/src/Record/HlsRecorder.h index 12ff668e..49965b1d 100644 --- a/src/Record/HlsRecorder.h +++ b/src/Record/HlsRecorder.h @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2016 The ZLMediaKit project authors. All Rights Reserved. * * This file is part of ZLMediaKit(https://github.com/xia-chu/ZLMediaKit). @@ -11,7 +11,8 @@ #ifndef HLSRECORDER_H #define HLSRECORDER_H -#include "HlsMakerImp.h" +//#include "HlsMakerImp.h" +#include "HlsMakerImpSub.h" #include "MPEG.h" #include "Common/config.h" @@ -26,9 +27,9 @@ public: GET_CONFIG(bool, hlsKeep, Hls::kSegmentKeep); GET_CONFIG(uint32_t, hlsBufSize, Hls::kFileBufSize); GET_CONFIG(float, hlsDuration, Hls::kSegmentDuration); - + // _hls = std::make_shared(m3u8_file, params, hlsBufSize, hlsDuration, hlsNum, hlsKeep); _option = option; - _hls = std::make_shared(m3u8_file, params, hlsBufSize, hlsDuration, hlsNum, hlsKeep); + _hls = std::make_shared(m3u8_file, params, hlsBufSize, hlsDuration, hlsNum, hlsKeep); //清空上次的残余文件 _hls->clearCache(); } @@ -73,7 +74,12 @@ public: //缓存尚未清空时,还允许触发inputFrame函数,以便及时清空缓存 return _option.hls_demand ? (_clear_cache ? true : _enabled) : true; } + void startRecord(bool flag) { + _hls->startRecord(flag); + _isRecord = flag; + } + bool getRecordFlag() { return _isRecord; } private: void onWrite(std::shared_ptr buffer, uint64_t timestamp, bool key_pos) override { if (!buffer) { @@ -86,8 +92,10 @@ private: private: bool _enabled = true; bool _clear_cache = false; + //std::shared_ptr _hls; ProtocolOption _option; - std::shared_ptr _hls; + std::shared_ptr _hls; + bool _isRecord = false; }; }//namespace mediakit #endif //HLSRECORDER_H diff --git a/src/Record/MP4Recorder.cpp b/src/Record/MP4Recorder.cpp index fd7c8955..186b8fdc 100644 --- a/src/Record/MP4Recorder.cpp +++ b/src/Record/MP4Recorder.cpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2016 The ZLMediaKit project authors. All Rights Reserved. * * This file is part of ZLMediaKit(https://github.com/xia-chu/ZLMediaKit). @@ -39,7 +39,7 @@ MP4Recorder::~MP4Recorder() { } void MP4Recorder::createFile() { - closeFile(); + //closeFile(); auto date = getTimeStr("%Y-%m-%d"); auto time = getTimeStr("%H-%M-%S"); auto full_path_tmp = _folder_path + date + "/." + time + ".mp4"; @@ -93,7 +93,7 @@ void MP4Recorder::asyncClose() { void MP4Recorder::closeFile() { if (_muxer) { - asyncClose(); + //asyncClose(); _muxer = nullptr; } } @@ -119,7 +119,7 @@ bool MP4Recorder::inputFrame(const Frame::Ptr &frame) { // 2、到了切片时间,并且只有音频 // 3、到了切片时间,有视频并且遇到视频的关键帧 _last_dts = 0; - createFile(); + //createFile(); } } @@ -140,7 +140,7 @@ bool MP4Recorder::addTrack(const Track::Ptr &track) { } void MP4Recorder::resetTracks() { - closeFile(); + //closeFile(); _tracks.clear(); _have_video = false; }