2019-12-04 10:45:38 +08:00
|
|
|
|
/*
|
|
|
|
|
* MIT License
|
|
|
|
|
*
|
|
|
|
|
* Copyright (c) 2016-2019 xiongziliang <771730766@qq.com>
|
|
|
|
|
*
|
|
|
|
|
* This file is part of ZLMediaKit(https://github.com/xiongziliang/ZLMediaKit).
|
|
|
|
|
*
|
|
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
|
|
|
* in the Software without restriction, including without limitation the rights
|
|
|
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
|
|
|
* furnished to do so, subject to the following conditions:
|
|
|
|
|
*
|
|
|
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
|
|
|
* copies or substantial portions of the Software.
|
|
|
|
|
*
|
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
|
|
* SOFTWARE.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include "Recorder.h"
|
|
|
|
|
#include "Common/config.h"
|
2019-12-05 10:47:23 +08:00
|
|
|
|
#include "Common/MediaSource.h"
|
2019-12-04 10:45:38 +08:00
|
|
|
|
#include "MP4Recorder.h"
|
|
|
|
|
#include "HlsRecorder.h"
|
|
|
|
|
|
|
|
|
|
using namespace toolkit;
|
|
|
|
|
|
|
|
|
|
namespace mediakit {
|
|
|
|
|
|
2019-12-04 18:36:30 +08:00
|
|
|
|
MediaSinkInterface *createHlsRecorder(const string &strVhost_tmp, const string &strApp, const string &strId) {
|
2019-12-04 10:45:38 +08:00
|
|
|
|
#if defined(ENABLE_HLS)
|
|
|
|
|
GET_CONFIG(bool, enableVhost, General::kEnableVhost);
|
|
|
|
|
GET_CONFIG(string, hlsPath, Hls::kFilePath);
|
|
|
|
|
|
|
|
|
|
string strVhost = strVhost_tmp;
|
|
|
|
|
if (trim(strVhost).empty()) {
|
|
|
|
|
//如果strVhost为空,则强制为默认虚拟主机
|
|
|
|
|
strVhost = DEFAULT_VHOST;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string m3u8FilePath;
|
|
|
|
|
string params;
|
|
|
|
|
if (enableVhost) {
|
|
|
|
|
m3u8FilePath = strVhost + "/" + strApp + "/" + strId + "/hls.m3u8";
|
|
|
|
|
params = string(VHOST_KEY) + "=" + strVhost;
|
|
|
|
|
} else {
|
|
|
|
|
m3u8FilePath = strApp + "/" + strId + "/hls.m3u8";
|
|
|
|
|
}
|
|
|
|
|
m3u8FilePath = File::absolutePath(m3u8FilePath, hlsPath);
|
|
|
|
|
return new HlsRecorder(m3u8FilePath, params);
|
|
|
|
|
#else
|
|
|
|
|
return nullptr;
|
|
|
|
|
#endif //defined(ENABLE_HLS)
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-04 18:36:30 +08:00
|
|
|
|
MediaSinkInterface *createMP4Recorder(const string &strVhost_tmp, const string &strApp, const string &strId) {
|
2019-12-04 10:45:38 +08:00
|
|
|
|
#if defined(ENABLE_MP4RECORD)
|
|
|
|
|
GET_CONFIG(bool, enableVhost, General::kEnableVhost);
|
|
|
|
|
GET_CONFIG(string, recordPath, Record::kFilePath);
|
|
|
|
|
GET_CONFIG(string, recordAppName, Record::kAppName);
|
|
|
|
|
|
|
|
|
|
string strVhost = strVhost_tmp;
|
|
|
|
|
if (trim(strVhost).empty()) {
|
|
|
|
|
//如果strVhost为空,则强制为默认虚拟主机
|
|
|
|
|
strVhost = DEFAULT_VHOST;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string mp4FilePath;
|
|
|
|
|
if (enableVhost) {
|
|
|
|
|
mp4FilePath = strVhost + "/" + recordAppName + "/" + strApp + "/" + strId + "/";
|
|
|
|
|
} else {
|
|
|
|
|
mp4FilePath = recordAppName + "/" + strApp + "/" + strId + "/";
|
|
|
|
|
}
|
|
|
|
|
mp4FilePath = File::absolutePath(mp4FilePath, recordPath);
|
|
|
|
|
return new MP4Recorder(mp4FilePath, strVhost, strApp, strId);
|
|
|
|
|
#else
|
|
|
|
|
return nullptr;
|
|
|
|
|
#endif //defined(ENABLE_MP4RECORD)
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-04 18:36:30 +08:00
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
|
|
class RecorderHelper {
|
|
|
|
|
public:
|
|
|
|
|
typedef std::shared_ptr<RecorderHelper> Ptr;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 构建函数
|
|
|
|
|
* @param bContinueRecord false表明hls录制从头开始录制(意味着hls临时文件在媒体反注册时会被删除)
|
|
|
|
|
*/
|
2019-12-05 10:47:23 +08:00
|
|
|
|
RecorderHelper(const MediaSinkInterface::Ptr &recorder, bool bContinueRecord) {
|
2019-12-04 18:36:30 +08:00
|
|
|
|
_recorder = recorder;
|
|
|
|
|
_continueRecord = bContinueRecord;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
~RecorderHelper() {
|
|
|
|
|
resetTracks();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 附则于track上
|
2019-12-04 18:58:19 +08:00
|
|
|
|
void attachTracks(vector<Track::Ptr> &&tracks, const string &schema){
|
2019-12-04 18:36:30 +08:00
|
|
|
|
if(isTracksSame(tracks)){
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
resetTracks();
|
|
|
|
|
_tracks = std::move(tracks);
|
2019-12-04 18:58:19 +08:00
|
|
|
|
_schema = schema;
|
2019-12-04 18:36:30 +08:00
|
|
|
|
for (auto &track : _tracks) {
|
|
|
|
|
_recorder->addTrack(track);
|
|
|
|
|
track->addDelegate(_recorder);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 判断新的tracks是否与之前的一致
|
|
|
|
|
bool isTracksSame(const vector<Track::Ptr> &tracks){
|
|
|
|
|
if(tracks.size() != _tracks.size()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
int i = 0;
|
|
|
|
|
for(auto &track : tracks){
|
|
|
|
|
if(track != _tracks[i++]){
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 重置所有track
|
|
|
|
|
void resetTracks(){
|
|
|
|
|
if(_tracks.empty()){
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
for (auto &track : _tracks) {
|
|
|
|
|
track->delDelegate(_recorder.get());
|
|
|
|
|
}
|
|
|
|
|
_tracks.clear();
|
|
|
|
|
_recorder->resetTracks();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 返回false表明hls录制从头开始录制(意味着hls临时文件在媒体反注册时会被删除)
|
|
|
|
|
bool continueRecord(){
|
|
|
|
|
return _continueRecord;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool isRecording() {
|
|
|
|
|
return !_tracks.empty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const string &getSchema() const{
|
|
|
|
|
return _schema;
|
|
|
|
|
}
|
|
|
|
|
private:
|
|
|
|
|
MediaSinkInterface::Ptr _recorder;
|
|
|
|
|
vector<Track::Ptr> _tracks;
|
|
|
|
|
bool _continueRecord;
|
|
|
|
|
string _schema;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
template<Recorder::type type>
|
|
|
|
|
class MediaSourceWatcher {
|
|
|
|
|
public:
|
|
|
|
|
static MediaSourceWatcher& Instance(){
|
|
|
|
|
static MediaSourceWatcher instance;
|
|
|
|
|
return instance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Recorder::status getRecordStatus(const string &vhost, const string &app, const string &stream_id) {
|
|
|
|
|
return getRecordStatus_l(getRecorderKey(vhost, app, stream_id));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int startRecord(const string &vhost, const string &app, const string &stream_id, bool waitForRecord, bool continueRecord) {
|
|
|
|
|
auto key = getRecorderKey(vhost, app, stream_id);
|
|
|
|
|
lock_guard<decltype(_recorder_mtx)> lck(_recorder_mtx);
|
|
|
|
|
if (getRecordStatus_l(key) != Recorder::status_not_record) {
|
|
|
|
|
// 已经在录制了
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string schema;
|
|
|
|
|
auto tracks = findTracks(vhost, app, stream_id,schema);
|
|
|
|
|
if (!waitForRecord && tracks.empty()) {
|
|
|
|
|
// 暂时无法开启录制
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto recorder = MediaSinkInterface::Ptr(createRecorder(vhost, app, stream_id));
|
|
|
|
|
if (!recorder) {
|
|
|
|
|
// 创建录制器失败
|
|
|
|
|
return -2;
|
|
|
|
|
}
|
2019-12-05 10:47:23 +08:00
|
|
|
|
auto helper = std::make_shared<RecorderHelper>(recorder, continueRecord);
|
|
|
|
|
if(tracks.size()){
|
|
|
|
|
helper->attachTracks(std::move(tracks),schema);
|
|
|
|
|
}
|
|
|
|
|
_recorder_map[key] = std::move(helper);
|
2019-12-04 18:36:30 +08:00
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-05 14:31:44 +08:00
|
|
|
|
bool stopRecord(const string &vhost, const string &app, const string &stream_id) {
|
2019-12-04 18:36:30 +08:00
|
|
|
|
lock_guard<decltype(_recorder_mtx)> lck(_recorder_mtx);
|
2019-12-05 14:31:44 +08:00
|
|
|
|
return _recorder_map.erase(getRecorderKey(vhost, app, stream_id));
|
2019-12-04 18:36:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
2019-12-04 19:15:48 +08:00
|
|
|
|
void stopAll(){
|
|
|
|
|
lock_guard<decltype(_recorder_mtx)> lck(_recorder_mtx);
|
|
|
|
|
_recorder_map.clear();
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-04 18:36:30 +08:00
|
|
|
|
private:
|
|
|
|
|
MediaSourceWatcher(){
|
2019-12-23 14:20:49 +08:00
|
|
|
|
//保存NoticeCenter的强引用,防止在MediaSourceWatcher单例释放前释放NoticeCenter单例
|
|
|
|
|
_notice_center = NoticeCenter::Instance().shared_from_this();
|
|
|
|
|
_notice_center->addListener(this,Broadcast::kBroadcastMediaChanged,[this](BroadcastMediaChangedArgs){
|
2019-12-26 11:53:19 +08:00
|
|
|
|
if(!bRegist){
|
|
|
|
|
removeRecorder(sender);
|
2019-12-04 18:36:30 +08:00
|
|
|
|
}
|
|
|
|
|
});
|
2019-12-23 14:20:49 +08:00
|
|
|
|
_notice_center->addListener(this,Broadcast::kBroadcastMediaResetTracks,[this](BroadcastMediaResetTracksArgs){
|
2019-12-26 11:53:19 +08:00
|
|
|
|
addRecorder(sender);
|
2019-12-04 18:36:30 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
~MediaSourceWatcher(){
|
2019-12-23 14:20:49 +08:00
|
|
|
|
_notice_center->delListener(this,Broadcast::kBroadcastMediaChanged);
|
|
|
|
|
_notice_center->delListener(this,Broadcast::kBroadcastMediaResetTracks);
|
2019-12-04 18:36:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
2019-12-26 11:53:19 +08:00
|
|
|
|
void addRecorder(MediaSource &sender){
|
2019-12-24 14:08:16 +08:00
|
|
|
|
auto key = getRecorderKey(sender.getVhost(),sender.getApp(),sender.getId());
|
2019-12-04 18:36:30 +08:00
|
|
|
|
lock_guard<decltype(_recorder_mtx)> lck(_recorder_mtx);
|
|
|
|
|
auto it = _recorder_map.find(key);
|
|
|
|
|
if(it == _recorder_map.end()){
|
|
|
|
|
// 录像记录不存在
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-24 14:08:16 +08:00
|
|
|
|
if(!it->second->isRecording() || it->second->getSchema() == sender.getSchema()){
|
2019-12-05 10:47:23 +08:00
|
|
|
|
// 绑定的协议一致或者并未正在录制则替换tracks
|
2019-12-26 11:53:19 +08:00
|
|
|
|
auto tracks = sender.getTracks(needTrackReady());
|
2019-12-05 10:47:23 +08:00
|
|
|
|
if (!tracks.empty()) {
|
2019-12-24 14:08:16 +08:00
|
|
|
|
it->second->attachTracks(std::move(tracks),sender.getSchema());
|
2019-12-05 10:47:23 +08:00
|
|
|
|
}
|
2019-12-04 18:36:30 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-26 11:53:19 +08:00
|
|
|
|
void removeRecorder(MediaSource &sender){
|
2019-12-24 14:08:16 +08:00
|
|
|
|
auto key = getRecorderKey(sender.getVhost(),sender.getApp(),sender.getId());
|
2019-12-04 18:36:30 +08:00
|
|
|
|
lock_guard<decltype(_recorder_mtx)> lck(_recorder_mtx);
|
|
|
|
|
auto it = _recorder_map.find(key);
|
2019-12-24 14:08:16 +08:00
|
|
|
|
if(it == _recorder_map.end() || it->second->getSchema() != sender.getSchema()){
|
2019-12-05 10:47:23 +08:00
|
|
|
|
// 录像记录不存在或绑定的协议不一致
|
2019-12-04 18:36:30 +08:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(it->second->continueRecord()){
|
|
|
|
|
// 如果可以继续录制,那么只重置tracks,不删除对象
|
|
|
|
|
it->second->resetTracks();
|
|
|
|
|
}else{
|
|
|
|
|
// 删除对象(意味着可能删除hls临时文件)
|
2019-12-04 18:46:31 +08:00
|
|
|
|
_recorder_map.erase(it);
|
2019-12-04 18:36:30 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Recorder::status getRecordStatus_l(const string &key) {
|
|
|
|
|
auto it = _recorder_map.find(key);
|
|
|
|
|
if (it == _recorder_map.end()) {
|
|
|
|
|
return Recorder::status_not_record;
|
|
|
|
|
}
|
2019-12-04 18:46:31 +08:00
|
|
|
|
return it->second->isRecording() ? Recorder::status_recording : Recorder::status_wait_record;
|
2019-12-04 18:36:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 查找MediaSource以便录制
|
|
|
|
|
vector<Track::Ptr> findTracks(const string &vhost, const string &app, const string &stream_id,string &schema) {
|
|
|
|
|
auto src = MediaSource::find(RTMP_SCHEMA, vhost, app, stream_id);
|
|
|
|
|
if (src) {
|
2019-12-26 11:53:19 +08:00
|
|
|
|
auto ret = src->getTracks(needTrackReady());
|
2019-12-04 18:36:30 +08:00
|
|
|
|
if (!ret.empty()) {
|
|
|
|
|
schema = RTMP_SCHEMA;
|
|
|
|
|
return std::move(ret);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
src = MediaSource::find(RTSP_SCHEMA, vhost, app, stream_id);
|
|
|
|
|
if (src) {
|
|
|
|
|
schema = RTSP_SCHEMA;
|
2019-12-26 11:53:19 +08:00
|
|
|
|
return src->getTracks(needTrackReady());
|
2019-12-04 18:36:30 +08:00
|
|
|
|
}
|
|
|
|
|
return vector<Track::Ptr>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string getRecorderKey(const string &vhost, const string &app, const string &stream_id) {
|
|
|
|
|
return vhost + "/" + app + "/" + stream_id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MediaSinkInterface *createRecorder(const string &vhost, const string &app, const string &stream_id) {
|
|
|
|
|
MediaSinkInterface *ret = nullptr;
|
|
|
|
|
switch (type) {
|
|
|
|
|
case Recorder::type_hls:
|
|
|
|
|
ret = createHlsRecorder(vhost, app, stream_id);
|
|
|
|
|
break;
|
|
|
|
|
case Recorder::type_mp4:
|
|
|
|
|
ret = createMP4Recorder(vhost, app, stream_id);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if(!ret){
|
2019-12-05 10:47:23 +08:00
|
|
|
|
WarnL << "can not create recorder of type: " << type;
|
2019-12-04 18:36:30 +08:00
|
|
|
|
}
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
2019-12-26 11:53:19 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 有些录制类型不需要track就绪即可录制
|
|
|
|
|
*/
|
|
|
|
|
bool needTrackReady(){
|
|
|
|
|
switch (type){
|
|
|
|
|
case Recorder::type_hls:
|
|
|
|
|
return false;
|
|
|
|
|
case Recorder::type_mp4:
|
|
|
|
|
return true;
|
|
|
|
|
default:
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-12-04 18:36:30 +08:00
|
|
|
|
private:
|
|
|
|
|
recursive_mutex _recorder_mtx;
|
2019-12-23 14:20:49 +08:00
|
|
|
|
NoticeCenter::Ptr _notice_center;
|
2019-12-04 18:36:30 +08:00
|
|
|
|
unordered_map<string, RecorderHelper::Ptr> _recorder_map;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Recorder::status Recorder::getRecordStatus(Recorder::type type, const string &vhost, const string &app, const string &stream_id) {
|
|
|
|
|
switch (type){
|
|
|
|
|
case type_mp4:
|
|
|
|
|
return MediaSourceWatcher<type_mp4>::Instance().getRecordStatus(vhost,app,stream_id);
|
|
|
|
|
case type_hls:
|
|
|
|
|
return MediaSourceWatcher<type_hls>::Instance().getRecordStatus(vhost,app,stream_id);
|
|
|
|
|
}
|
|
|
|
|
return status_not_record;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int Recorder::startRecord(Recorder::type type, const string &vhost, const string &app, const string &stream_id, bool waitForRecord, bool continueRecord) {
|
|
|
|
|
switch (type){
|
|
|
|
|
case type_mp4:
|
|
|
|
|
return MediaSourceWatcher<type_mp4>::Instance().startRecord(vhost,app,stream_id,waitForRecord,continueRecord);
|
|
|
|
|
case type_hls:
|
|
|
|
|
return MediaSourceWatcher<type_hls>::Instance().startRecord(vhost,app,stream_id,waitForRecord,continueRecord);
|
|
|
|
|
}
|
2019-12-05 10:47:23 +08:00
|
|
|
|
WarnL << "unknown record type: " << type;
|
2019-12-04 18:36:30 +08:00
|
|
|
|
return -3;
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-05 14:31:44 +08:00
|
|
|
|
bool Recorder::stopRecord(Recorder::type type, const string &vhost, const string &app, const string &stream_id) {
|
2019-12-04 18:36:30 +08:00
|
|
|
|
switch (type){
|
|
|
|
|
case type_mp4:
|
|
|
|
|
return MediaSourceWatcher<type_mp4>::Instance().stopRecord(vhost,app,stream_id);
|
|
|
|
|
case type_hls:
|
|
|
|
|
return MediaSourceWatcher<type_hls>::Instance().stopRecord(vhost,app,stream_id);
|
|
|
|
|
}
|
2019-12-05 14:31:44 +08:00
|
|
|
|
return false;
|
2019-12-04 18:36:30 +08:00
|
|
|
|
}
|
2019-12-04 10:45:38 +08:00
|
|
|
|
|
2019-12-04 19:15:48 +08:00
|
|
|
|
void Recorder::stopAll() {
|
|
|
|
|
MediaSourceWatcher<type_hls>::Instance().stopAll();
|
|
|
|
|
MediaSourceWatcher<type_mp4>::Instance().stopAll();
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-04 10:45:38 +08:00
|
|
|
|
} /* namespace mediakit */
|