Recording functionality by Diederick Huijbers, slightly tweaked.
This commit is contained in:
parent
c0b5f0d4b1
commit
1c3e143709
14 changed files with 550 additions and 10 deletions
|
@ -46,11 +46,13 @@ namespace HTTP {
|
|||
unsigned int length;
|
||||
bool headerOnly; ///< If true, do not parse body if the length is a known size.
|
||||
bool bufferChunks;
|
||||
//this bool was private
|
||||
bool sendingChunks;
|
||||
|
||||
private:
|
||||
bool seenHeaders;
|
||||
bool seenReq;
|
||||
bool getChunks;
|
||||
bool sendingChunks;
|
||||
unsigned int doingChunk;
|
||||
bool parse(std::string & HTTPbuffer);
|
||||
void parseVars(std::string data);
|
||||
|
|
227
lib/stream.cpp
227
lib/stream.cpp
|
@ -16,6 +16,12 @@
|
|||
#include "dtsc.h"
|
||||
#include "triggers.h"//LTS
|
||||
|
||||
/* roxlu-begin */
|
||||
static std::string strftime_now(const std::string& format);
|
||||
static void replace_str(std::string& str, const std::string& from, const std::string& to);
|
||||
static void replace_variables(std::string& str);
|
||||
/* roxlu-end */
|
||||
|
||||
std::string Util::getTmpFolder() {
|
||||
std::string dir;
|
||||
char * tmp_char = 0;
|
||||
|
@ -287,3 +293,224 @@ bool Util::startInput(std::string streamname, std::string filename, bool forkFir
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* roxlu-begin */
|
||||
int Util::startRecording(std::string streamname) {
|
||||
|
||||
sanitizeName(streamname);
|
||||
if (streamname.size() > 100){
|
||||
FAIL_MSG("Stream opening denied: %s is longer than 100 characters (%lu).", streamname.c_str(), streamname.size());
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Attempt to load up configuration and find this stream
|
||||
IPC::sharedPage mistConfOut("!mistConfig", DEFAULT_CONF_PAGE_SIZE);
|
||||
IPC::semaphore configLock("!mistConfLock", O_CREAT | O_RDWR, ACCESSPERMS, 1);
|
||||
|
||||
//Lock the config to prevent race conditions and corruption issues while reading
|
||||
configLock.wait();
|
||||
DTSC::Scan config = DTSC::Scan(mistConfOut.mapped, mistConfOut.len);
|
||||
|
||||
//Abort if no config available
|
||||
if (!config){
|
||||
FAIL_MSG("Configuration not available, aborting! Is MistController running?");
|
||||
configLock.post();//unlock the config semaphore
|
||||
return -2;
|
||||
}
|
||||
|
||||
//Find stream base name
|
||||
std::string smp = streamname.substr(0, streamname.find_first_of("+ "));
|
||||
DTSC::Scan streamCfg = config.getMember("streams").getMember(smp);
|
||||
if (!streamCfg){
|
||||
DEBUG_MSG(DLVL_HIGH, "Stream %s not configured - attempting to ignore", streamname.c_str());
|
||||
configLock.post();
|
||||
return -3;
|
||||
}
|
||||
|
||||
// When we have a validate trigger, we execute that first before we continue.
|
||||
if (Triggers::shouldTrigger("RECORDING_VALIDATE", streamname)) {
|
||||
std::string validate_result;
|
||||
Triggers::doTrigger("RECORDING_VALIDATE", streamname, streamname.c_str(), false, validate_result);
|
||||
INFO_MSG("RECORDING_VALIDATE returned: %s", validate_result.c_str());
|
||||
if (validate_result == "0") {
|
||||
INFO_MSG("RECORDING_VALIDATE: the hook returned 0 so we're not going to create a recording.");
|
||||
configLock.post();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Should we start an flv output? (We allow hooks to specify custom filenames)
|
||||
DTSC::Scan recordFilenameConf = streamCfg.getMember("record");
|
||||
std::string recordFilename;
|
||||
|
||||
if (Triggers::shouldTrigger("RECORDING_FILEPATH", streamname)) {
|
||||
|
||||
std::string payload = streamname;
|
||||
std::string filepath_response;
|
||||
Triggers::doTrigger("RECORDING_FILEPATH", payload, streamname.c_str(), false, filepath_response); /* @todo do we need to handle the return of doTrigger? */
|
||||
|
||||
if (filepath_response.size() < 1024) { /* @todo is there a MAX_FILEPATH somewhere? */
|
||||
recordFilename = filepath_response;
|
||||
}
|
||||
else {
|
||||
FAIL_MSG("The RECORDING_FILEPATH trigger returned a filename which is bigger then our allowed max filename size. Not using returned filepath from hook.");
|
||||
}
|
||||
}
|
||||
|
||||
// No filename set through trigger, so use the one one from the stream config.
|
||||
if (recordFilename.size() == 0) {
|
||||
recordFilename = recordFilenameConf.asString();
|
||||
}
|
||||
|
||||
/*if (recordFilename.size() == 0
|
||||
|| recordFilename.substr(recordFilename.find_last_of(".") + 1) != "flv")
|
||||
{
|
||||
configLock.post();
|
||||
return -4;
|
||||
}*/
|
||||
|
||||
// The filename can hold variables like current time etc..
|
||||
replace_variables(recordFilename);
|
||||
|
||||
INFO_MSG("Filepath that we use for the recording: %s", recordFilename.c_str());
|
||||
//to change hardcoding
|
||||
//determine extension, first find the '.' for extension
|
||||
size_t pointPlace = recordFilename.rfind(".");
|
||||
if (pointPlace == std::string::npos){
|
||||
FAIL_MSG("no extension found in output name. Aborting recording.");
|
||||
return -1;
|
||||
}
|
||||
std::string fileExtension = recordFilename.substr(pointPlace+1);
|
||||
DTSC::Scan outputs = config.getMember("capabilities").getMember("connectors");
|
||||
DTSC::Scan output;
|
||||
std::string output_filepath = "";
|
||||
unsigned int outputs_size = outputs.getSize();
|
||||
HIGH_MSG("Recording outputs %d",outputs_size);
|
||||
for (unsigned int i = 0; i<outputs_size; ++i){
|
||||
output = outputs.getIndice(i);
|
||||
HIGH_MSG("Checking output: %s",output.getMember("name").asString().c_str());
|
||||
if (output.getMember("canRecord")){
|
||||
HIGH_MSG("Output %s can record!", output.getMember("name").asString().c_str());
|
||||
DTSC::Scan recTypes = output.getMember("canRecord");
|
||||
unsigned int recTypesLength = recTypes.getSize();
|
||||
bool breakOuterLoop = false;
|
||||
for (unsigned int o = 0; o<recTypesLength; ++o){
|
||||
if (recTypes.getIndice(o).asString() == fileExtension){
|
||||
HIGH_MSG("Output %s can record %s!", output.getMember("name").asString().c_str(), fileExtension.c_str());
|
||||
output_filepath = Util::getMyPath() + "MistOut" + output.getMember("name").asString();
|
||||
breakOuterLoop = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (breakOuterLoop) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (output_filepath == ""){
|
||||
FAIL_MSG("No output found for filetype %s.", fileExtension.c_str());
|
||||
return -4;
|
||||
}
|
||||
// Start output.
|
||||
char* argv[] = {
|
||||
(char*)output_filepath.c_str(),
|
||||
(char*)"--stream", (char*)streamname.c_str(),
|
||||
(char*)"--outputFilename", (char*)recordFilename.c_str(),
|
||||
(char*)NULL
|
||||
};
|
||||
|
||||
int pid = fork();
|
||||
if (pid == -1) {
|
||||
FAIL_MSG("Forking process for stream %s failed: %s", streamname.c_str(), strerror(errno));
|
||||
configLock.post();
|
||||
return -5;
|
||||
}
|
||||
|
||||
// Child process gets pid == 0
|
||||
if (pid == 0) {
|
||||
if (execvp(argv[0], argv) == -1) {
|
||||
FAIL_MSG("Failed to start MistOutFLV: %s", strerror(errno));
|
||||
configLock.post();
|
||||
return -6;
|
||||
}
|
||||
}
|
||||
|
||||
configLock.post();
|
||||
|
||||
return pid;
|
||||
}
|
||||
|
||||
static void replace(std::string& str, const std::string& from, const std::string& to) {
|
||||
if(from.empty()) {
|
||||
return;
|
||||
}
|
||||
size_t start_pos = 0;
|
||||
while((start_pos = str.find(from, start_pos)) != std::string::npos) {
|
||||
str.replace(start_pos, from.length(), to);
|
||||
start_pos += to.length();
|
||||
}
|
||||
}
|
||||
|
||||
static void replace_variables(std::string& str) {
|
||||
|
||||
char buffer[80] = { 0 };
|
||||
std::map<std::string, std::string> vars;
|
||||
std::string day = strftime_now("%d");
|
||||
std::string month = strftime_now("%m");
|
||||
std::string year = strftime_now("%Y");
|
||||
std::string hour = strftime_now("%H");
|
||||
std::string minute = strftime_now("%M");
|
||||
std::string seconds = strftime_now("%S");
|
||||
std::string datetime = year +"." +month +"." +day +"." +hour +"." +minute +"." +seconds;
|
||||
|
||||
if (0 == day.size()) {
|
||||
WARN_MSG("Failed to retrieve the current day with strftime_now().");
|
||||
}
|
||||
if (0 == month.size()) {
|
||||
WARN_MSG("Failed to retrieve the current month with strftime_now().");
|
||||
}
|
||||
if (0 == year.size()) {
|
||||
WARN_MSG("Failed to retrieve the current year with strftime_now().");
|
||||
}
|
||||
if (0 == hour.size()) {
|
||||
WARN_MSG("Failed to retrieve the current hour with strftime_now().");
|
||||
}
|
||||
if (0 == minute.size()) {
|
||||
WARN_MSG("Failed to retrieve the current minute with strftime_now().");
|
||||
}
|
||||
if (0 == seconds.size()) {
|
||||
WARN_MSG("Failed to retrieve the current seconds with strftime_now().");
|
||||
}
|
||||
|
||||
vars.insert(std::pair<std::string, std::string>("$day", day));
|
||||
vars.insert(std::pair<std::string, std::string>("$month", month));
|
||||
vars.insert(std::pair<std::string, std::string>("$year", year));
|
||||
vars.insert(std::pair<std::string, std::string>("$hour", hour));
|
||||
vars.insert(std::pair<std::string, std::string>("$minute", minute));
|
||||
vars.insert(std::pair<std::string, std::string>("$seconds", seconds));
|
||||
vars.insert(std::pair<std::string, std::string>("$datetime", datetime));
|
||||
|
||||
std::map<std::string, std::string>::iterator it = vars.begin();
|
||||
while (it != vars.end()) {
|
||||
replace(str, it->first, it->second);
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
static std::string strftime_now(const std::string& format) {
|
||||
|
||||
time_t rawtime;
|
||||
struct tm* timeinfo = NULL;
|
||||
char buffer [80] = { 0 };
|
||||
|
||||
time(&rawtime);
|
||||
timeinfo = localtime (&rawtime);
|
||||
|
||||
if (0 == strftime(buffer, 80, format.c_str(), timeinfo)) {
|
||||
FAIL_MSG("Call to stftime() failed with format: %s, maybe our buffer is not big enough (80 bytes).", format.c_str());
|
||||
return "";
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/* roxlu-end */
|
||||
|
|
|
@ -11,5 +11,8 @@ namespace Util {
|
|||
void sanitizeName(std::string & streamname);
|
||||
bool streamAlive(std::string & streamname);
|
||||
bool startInput(std::string streamname, std::string filename = "", bool forkFirst = true);
|
||||
/* roxlu-begin */
|
||||
int startRecording(std::string streamname);
|
||||
/* roxlu-end */
|
||||
JSON::Value getStreamConfig(std::string streamname);
|
||||
}
|
||||
|
|
|
@ -127,8 +127,9 @@ a[0]+" trigger?")){mist.data.config.triggers[a[0]].splice(a[1],1);mist.data.conf
|
|||
pointer:{main:n,index:"triggeron"},help:"For what event this trigger should activate.",type:"select",select:[["SYSTEM_START","SYSTEM_START: after MistServer boot"],["SYSTEM_STOP","SYSTEM_STOP: right before MistServer shutdown"],["SYSTEM_CONFIG","SYSTEM_CONFIG: after MistServer configurations have changed"],["OUTPUT_START","OUTPUT_START: right after the start command has been send to a protocol"],["OUTPUT_STOP","OUTPUT_STOP: right after the close command has been send to a protocol "],["STREAM_ADD",
|
||||
"STREAM_ADD: right before new stream configured"],["STREAM_CONFIG","STREAM_CONFIG: right before a stream configuration has changed"],["STREAM_REMOVE","STREAM_REMOVE: right before a stream has been deleted"],["STREAM_SOURCE","STREAM_SOURCE: right before stream source is loaded"],["STREAM_LOAD","STREAM_LOAD: right before stream input is loaded in memory"],["STREAM_READY","STREAM_READY: when the stream input is loaded and ready for playback"],["STREAM_UNLOAD","STREAM_UNLOAD: right before the stream input is removed from memory"],
|
||||
["STREAM_PUSH","STREAM_PUSH: right before an incoming push is accepted"],["STREAM_TRACK_ADD","STREAM_TRACK_ADD: right before a track will be added to a stream; e.g.: additional push received"],["STREAM_TRACK_REMOVE","STREAM_TRACK_REMOVE: right before a track will be removed track from a stream; e.g.: push timeout"],["STREAM_BUFFER","STREAM_BUFFER: when a buffer changes between mostly full or mostly empty"],["RTMP_PUSH_REWRITE","RTMP_PUSH_REWRITE: allows rewriting of RTMP push URLs from external to internal representation before further parsing"],
|
||||
["CONN_OPEN","CONN_OPEN: right after a new incoming connection has been received"],["CONN_CLOSE","CONN_CLOSE: right after a connection has been closed"],["CONN_PLAY","CONN_PLAY: right before a stream playback of a connection"]],LTSonly:!0,"function":function(){switch($(this).getval()){case "SYSTEM_START":case "SYSTEM_STOP":case "SYSTEM_CONFIG":case "OUTPUT_START":case "OUTPUT_STOP":case "RTMP_PUSH_REWRITE":$("[name=appliesto]").setval([]).closest(".UIelement").hide();break;default:$("[name=appliesto]").closest(".UIelement").show()}}},
|
||||
{label:"Applies to",pointer:{main:n,index:"appliesto"},help:"For triggers that can apply to specific streams, this value decides what streams they are triggered for. (none checked = always triggered)",type:"checklist",checklist:Object.keys(mist.data.streams),LTSonly:!0},$("<br>"),{label:"Handler (URL or executable)",help:"This can be either an HTTP URL or a full path to an executable.",pointer:{main:n,index:"url"},validate:["required"],type:"str",LTSonly:!0},{label:"Blocking",type:"checkbox",help:"If checked, pauses processing and uses the response of the handler. If the response does not start with 1, true, yes or cont, further processing is aborted. If unchecked, processing is never paused and the response is not checked.",
|
||||
["RECORDING_VALIDATE","RECORDING_VALIDATE: before recording, check if we this stream is allowed to record."],["RECORDING_FILEPATH","RECORDING_FILEPATH: before recording, hook can return filename to be used. "],["RECORDING_START","RECORDING_START: started a recording"],["RECORDING_STOP","RECORDING_STOP: stopped a recording"],["CONN_OPEN","CONN_OPEN: right after a new incoming connection has been received"],["CONN_CLOSE","CONN_CLOSE: right after a connection has been closed"],["CONN_PLAY","CONN_PLAY: right before a stream playback of a connection"]],
|
||||
LTSonly:!0,"function":function(){switch($(this).getval()){case "SYSTEM_START":case "SYSTEM_STOP":case "SYSTEM_CONFIG":case "OUTPUT_START":case "OUTPUT_STOP":case "RTMP_PUSH_REWRITE":$("[name=appliesto]").setval([]).closest(".UIelement").hide();break;default:$("[name=appliesto]").closest(".UIelement").show()}}},{label:"Applies to",pointer:{main:n,index:"appliesto"},help:"For triggers that can apply to specific streams, this value decides what streams they are triggered for. (none checked = always triggered)",
|
||||
type:"checklist",checklist:Object.keys(mist.data.streams),LTSonly:!0},$("<br>"),{label:"Handler (URL or executable)",help:"This can be either an HTTP URL or a full path to an executable.",pointer:{main:n,index:"url"},validate:["required"],type:"str",LTSonly:!0},{label:"Blocking",type:"checkbox",help:"If checked, pauses processing and uses the response of the handler. If the response does not start with 1, true, yes or cont, further processing is aborted. If unchecked, processing is never paused and the response is not checked.",
|
||||
pointer:{main:n,index:"async"},LTSonly:!0},{label:"Default response",type:"str",help:"For blocking requests, the default response in case the handler cannot be executed for any reason.",pointer:{main:n,index:"default"},LTSonly:!0},{type:"buttons",buttons:[{type:"cancel",label:"Cancel","function":function(){UI.navto("Triggers")}},{type:"save",label:"Save","function":function(){c&&mist.data.config.triggers[c[0]].splice(c[1],1);var a=[n.url,n.async?true:false,typeof n.appliesto!="undefined"?n.appliesto:
|
||||
[]];typeof n["default"]!="undefined"&&a.push(n["default"]);n.triggeron in mist.data.config.triggers||(mist.data.config.triggers[n.triggeron]=[]);mist.data.config.triggers[n.triggeron].push(a);mist.send(function(){UI.navto("Triggers")},{config:mist.data.config})}}]}]));$("[name=triggeron]").trigger("change");break;case "Logs":b.append(UI.buildUI([{type:"help",help:"Here you have an overview of all edited settings within MistServer and possible warnings or errors MistServer has encountered. MistServer stores up to 100 logs at a time."},
|
||||
{label:"Refresh every",type:"select",select:[[10,"10 seconds"],[30,"30 seconds"],[60,"minute"],[300,"5 minutes"]],value:30,"function":function(){clearInterval(UI.interval);UI.interval.set(function(){mist.send(function(){V()})},$(this).val()*1E3)}}]));b.append($("<button>").text("Purge logs").click(function(){mist.send(function(){mist.data.log=[];UI.navto("Logs")},{clearstatlogs:true})}));g=$("<tbody>").css("font-size","0.9em");b.append($("<table>").append(g));var X=function(a){var b=$("<span>").text(a);
|
||||
|
|
|
@ -3570,6 +3570,10 @@ var UI = {
|
|||
['STREAM_TRACK_REMOVE', 'STREAM_TRACK_REMOVE: right before a track will be removed track from a stream; e.g.: push timeout'],
|
||||
['STREAM_BUFFER', 'STREAM_BUFFER: when a buffer changes between mostly full or mostly empty'],
|
||||
['RTMP_PUSH_REWRITE', 'RTMP_PUSH_REWRITE: allows rewriting of RTMP push URLs from external to internal representation before further parsing'],
|
||||
['RECORDING_VALIDATE', 'RECORDING_VALIDATE: before recording, check if we this stream is allowed to record.'],
|
||||
['RECORDING_FILEPATH', 'RECORDING_FILEPATH: before recording, hook can return filename to be used. '],
|
||||
['RECORDING_START', 'RECORDING_START: started a recording'],
|
||||
['RECORDING_STOP', 'RECORDING_STOP: stopped a recording'],
|
||||
['CONN_OPEN', 'CONN_OPEN: right after a new incoming connection has been received'],
|
||||
['CONN_CLOSE', 'CONN_CLOSE: right after a connection has been closed'],
|
||||
['CONN_PLAY', 'CONN_PLAY: right before a stream playback of a connection']
|
||||
|
|
|
@ -113,6 +113,7 @@ namespace Mist {
|
|||
cutTime = 0;
|
||||
segmentSize = 5000;
|
||||
hasPush = false;
|
||||
recordingPid = -1;
|
||||
}
|
||||
|
||||
inputBuffer::~inputBuffer(){
|
||||
|
@ -881,6 +882,58 @@ namespace Mist {
|
|||
}
|
||||
*/
|
||||
|
||||
/* roxlu-begin */
|
||||
// check if we have a video track with a keyframe, otherwise the mp4 output will fail.
|
||||
// @todo as the mp4 recording was not working perfectly I focussed on getting it
|
||||
// to work for .flv. This seems to work perfectly but ofc. we want to make it work
|
||||
// for .mp4 too at some point.
|
||||
bool has_keyframes = false;
|
||||
std::map<unsigned int, DTSC::Track>::iterator it = myMeta.tracks.begin();
|
||||
while (it != myMeta.tracks.end()) {
|
||||
|
||||
DTSC::Track& tr = it->second;
|
||||
if (tr.type != "video") {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tr.keys.size() > 0) {
|
||||
has_keyframes = true;
|
||||
break;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
|
||||
if (streamCfg
|
||||
&& streamCfg.getMember("record")
|
||||
&& streamCfg.getMember("record").asString().size() > 0
|
||||
&& has_keyframes
|
||||
)
|
||||
{
|
||||
|
||||
// @todo check if output is already running ?
|
||||
if (recordingPid == -1
|
||||
&& config != NULL
|
||||
)
|
||||
{
|
||||
|
||||
INFO_MSG("The stream %s has a value specified for the recording. "
|
||||
"We're goint to start an output and record into %s",
|
||||
config->getString("streamname").c_str(),
|
||||
streamCfg.getMember("record").asString().c_str());
|
||||
|
||||
recordingPid = Util::startRecording(config->getString("streamname"));
|
||||
if (recordingPid < 0) {
|
||||
FAIL_MSG("Failed to start the recording for %s", config->getString("streamname").c_str());
|
||||
// @todo shouldn't we do configList.post(), configLock.close() and return false?
|
||||
// @todo discuss with Jaron. 2015.09.26, remove this comment when discussed.
|
||||
}
|
||||
INFO_MSG("We started an output for recording with PID: %d", recordingPid);
|
||||
}
|
||||
}
|
||||
/* roxlu-end */
|
||||
|
||||
|
||||
/*LTS-END*/
|
||||
configLock.post();
|
||||
configLock.close();
|
||||
|
|
|
@ -46,6 +46,10 @@ namespace Mist {
|
|||
long long int recBpos;/*LTS*/
|
||||
//This is used for an ugly fix to prevent metadata from dissapearing in some cases.
|
||||
std::map<unsigned long, std::string> initData;
|
||||
|
||||
/* begin-roxlu */
|
||||
int recordingPid; // pid of the process that does the recording. Currently only MP4 supported.
|
||||
/* end-roxlu */
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,62 @@
|
|||
/// Recording to file
|
||||
///
|
||||
/// Currently MistServer has basic support for recording for which the
|
||||
/// functionality is spread over a couple of files. The general flow in
|
||||
/// mist (this is my understanding and I'm a newb to MistServer, roxlu),
|
||||
/// is like this:
|
||||
///
|
||||
/// The controller creates a couple of protocol handlers, e.g. for
|
||||
/// RTMP. When a new live connection is made, an output is created through
|
||||
/// this protocol handler. In the case of a live source, all received
|
||||
/// data is passed into a inputBuffer object (see input_buffer.cpp).
|
||||
///
|
||||
/// So, when the inputBuffer is created, the `setup()` function is
|
||||
/// called. In this function the `config` object is available that holds
|
||||
/// the configuration values for the specific stream. This is also where a
|
||||
/// recording gets initialized.
|
||||
///
|
||||
/// An recording is initialized by starting another output with a call to
|
||||
/// `startRecording()`. `startRecording()` forks the current process and
|
||||
/// then calls `execvp()` to take over the child process with
|
||||
/// e.g. `MistOutFLV()`. When `execvp()` starts the other process (that
|
||||
/// records the data), it passes the `--outputFilename` command line
|
||||
/// argument.
|
||||
///
|
||||
/// Each output checks if it's started with the `--outputFilename` flag;
|
||||
/// this is done in the constructor of `Output`. In Output, it opens the
|
||||
/// given filename and uses `dup2()` which makes sure that all `stdout`
|
||||
/// data is written into the recording file.
|
||||
///
|
||||
/// Though, because some or probably most outputs also write HTTP to
|
||||
/// stdout, I created the function `HTTPOutput::sendResponse()` which
|
||||
/// checks if the current output is creating a recording. When creating a
|
||||
/// recording it simply skips the HTTP output.
|
||||
///
|
||||
/// +-------------------------+
|
||||
/// | inputBuffer::setup() |
|
||||
/// +-------+-----------------+
|
||||
/// |
|
||||
/// o---- calls Util::startRecording() (stream.cpp)
|
||||
/// |
|
||||
/// v
|
||||
/// +------------------------+
|
||||
/// | stream::startRecording | -> Kicks off output app with --outputFilename
|
||||
/// +-------+----------------+
|
||||
/// |
|
||||
/// v
|
||||
/// +----------------+
|
||||
/// | MistOut[XXX] | -> Checks if started with --outputFilename,
|
||||
/// +----------------+ in Output::Output() and starts recording.
|
||||
///
|
||||
/// The following files contain updates that were made for the recording:
|
||||
///
|
||||
/// - stream.cpp - startRecording()
|
||||
/// - output.cpp - Output(), - added --outputFilename option
|
||||
/// ~Output(), - closes the filedescriptor if opened.
|
||||
/// openOutputFileForRecording() - opens the output file descriptor, uses dup2().
|
||||
/// closeOutputFileForRecording() - closes the output file descriptor.
|
||||
/// - input_buffer.cpp - setup() - executes an MistOut[XXX] app.
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
|
@ -41,6 +100,13 @@ namespace Mist {
|
|||
capa["optional"]["startpos"]["option"] = "--startPos";
|
||||
capa["optional"]["startpos"]["type"] = "uint";
|
||||
cfg->addOption("startpos", JSON::fromString("{\"arg\":\"uint\",\"default\":500,\"short\":\"P\",\"long\":\"startPos\",\"help\":\"For live, where in the buffer the stream starts playback by default. 0 = beginning, 1000 = end\"}"));
|
||||
/* begin-roxlu */
|
||||
capa["optional"]["outputfilename"]["type"] = "string";
|
||||
capa["optional"]["outputfilename"]["name"] = "outputfilename";
|
||||
capa["optional"]["outputfilename"]["help"] = "Name of the file into which we write the recording.";
|
||||
capa["optional"]["outputfilename"]["option"] = "--outputFilename";
|
||||
cfg->addOption("outputfilename", JSON::fromString("{\"arg\":\"string\",\"default\":\"\",\"short\":\"O\",\"long\":\"outputFilename\",\"help\":\"The name of the file that is used to record a stream.\"}"));
|
||||
/* end-roxlu */
|
||||
}
|
||||
|
||||
Output::Output(Socket::Connection & conn) : myConn(conn) {
|
||||
|
@ -63,6 +129,16 @@ namespace Mist {
|
|||
DEBUG_MSG(DLVL_WARN, "Warning: MistOut created with closed socket!");
|
||||
}
|
||||
sentHeader = false;
|
||||
/* begin-roxlu */
|
||||
outputFileDescriptor = -1;
|
||||
|
||||
// When the stream has a output filename defined we open it so we can start recording.
|
||||
if (config != NULL
|
||||
&& config->getString("outputfilename").size() != 0)
|
||||
{
|
||||
openOutputFileForRecording();
|
||||
}
|
||||
/* end-roxlu */
|
||||
}
|
||||
|
||||
void Output::setBlocking(bool blocking){
|
||||
|
@ -70,7 +146,13 @@ namespace Mist {
|
|||
myConn.setBlocking(isBlocking);
|
||||
}
|
||||
|
||||
Output::~Output(){}
|
||||
/*begin-roxlu*/
|
||||
Output::~Output(){
|
||||
if (config != NULL && config->getString("outputfilename").size() != 0){
|
||||
closeOutputFileForRecording();
|
||||
}
|
||||
}
|
||||
/*end-roxlu*/
|
||||
|
||||
void Output::updateMeta(){
|
||||
//read metadata from page to myMeta variable
|
||||
|
@ -287,6 +369,18 @@ namespace Mist {
|
|||
DEBUG_MSG(DLVL_MEDIUM, "Selected tracks: %s (%lu)", selected.str().c_str(), selectedTracks.size());
|
||||
}
|
||||
|
||||
/*begin-roxlu*/
|
||||
// Added this check while working on the recording, because when the output cant
|
||||
// select a track it means it won't be able to start the recording. Therefore
|
||||
// when we don't see this explicitly it makes debugging the recording feature
|
||||
// a bit painfull :)
|
||||
if (selectedTracks.size() == 0) {
|
||||
WARN_MSG("We didn't find any tracks which that we can use. selectedTrack.size() is 0.");
|
||||
for (std::map<unsigned int,DTSC::Track>::iterator trit = myMeta.tracks.begin(); trit != myMeta.tracks.end(); trit++){
|
||||
WARN_MSG("Found track/codec: %s", trit->second.codec.c_str());
|
||||
}
|
||||
}
|
||||
/*end-roxlu*/
|
||||
}
|
||||
|
||||
/// Clears the buffer, sets parseData to false, and generally makes not very much happen at all.
|
||||
|
@ -889,7 +983,8 @@ namespace Mist {
|
|||
buffer.insert(nxt);
|
||||
}else{
|
||||
//after ~10 seconds, give up and drop the track.
|
||||
DEBUG_MSG(DLVL_DEVEL, "Empty packet on track %u @ key %lu (next=%d) - could not reload, dropping track.", nxt.tid, nxtKeyNum[nxt.tid]+1, nextPage);
|
||||
//roxlu edited this line:
|
||||
DEBUG_MSG(DLVL_DEVEL, "Empty packet on track %u (%s) @ key %lu (next=%d) - could not reload, dropping track.", nxt.tid, myMeta.tracks[nxt.tid].type.c_str(), nxtKeyNum[nxt.tid]+1, nextPage);
|
||||
}
|
||||
//keep updating the metadata at 250ms intervals while waiting for more data
|
||||
Util::sleep(250);
|
||||
|
@ -1062,4 +1157,109 @@ namespace Mist {
|
|||
//just set the sentHeader bool to true, by default
|
||||
sentHeader = true;
|
||||
}
|
||||
|
||||
/*begin-roxlu*/
|
||||
bool Output::openOutputFileForRecording() {
|
||||
|
||||
if (NULL == config) {
|
||||
FAIL_MSG("Cannot open the output file for recording because the config member is NULL and we can't check if we actually want a recording.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// We won't open the output file when the user didn't set the outputfile through the admin.
|
||||
if (config->getString("outputfilename").size() == 0) {
|
||||
FAIL_MSG("Cannot open the output file for recording because the given name is empty.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (outputFileDescriptor != -1) {
|
||||
FAIL_MSG("Cannot open the output file for recording because it seems that it's already open. Make sure it's closed correctly.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// The RECORDING_START trigger needs to be execute before we open the file because
|
||||
// the trigger may need to create some directories where we need to save the recording.
|
||||
if (Triggers::shouldTrigger("RECORDING_START")) {
|
||||
|
||||
if (0 == config->getString("streamname").size()) {
|
||||
ERROR_MSG("Streamname is empty; the RECORDING_START trigger will not know what stream started it's recording. We do execute the trigger.");
|
||||
}
|
||||
|
||||
std::string payload = config->getString("streamname");
|
||||
Triggers::doTrigger("RECORDING_START", payload, streamName.c_str());
|
||||
}
|
||||
|
||||
// Open the output file.
|
||||
int flags = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
|
||||
int mode = O_RDWR | O_CREAT | O_TRUNC;
|
||||
|
||||
outputFileDescriptor = open(config->getString("outputfilename").c_str(), mode, flags);
|
||||
if (outputFileDescriptor < 0) {
|
||||
ERROR_MSG("Failed to open the file that we want to use to store the recording, error: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make a copy of the socket into outputFileDescriptor. Whenever we write to the socket we write to file.
|
||||
int r = dup2(outputFileDescriptor, myConn.getSocket());
|
||||
if (r == -1) {
|
||||
ERROR_MSG("Failed to create an alias for the socket using dup2: %s.", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
//make this output ready for recording to file
|
||||
onRecord();
|
||||
|
||||
INFO_MSG("Opened %s for recording.", config->getString("outputfilename").c_str());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Output::closeOutputFileForRecording() {
|
||||
|
||||
if (config == NULL) {
|
||||
ERROR_MSG("Config member is NULL, we cannot close the output file for the recording.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (outputFileDescriptor == -1) {
|
||||
ERROR_MSG("Requested to close the output file for the recording, but we're not making a recording.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config->getString("outputfilename").size() == 0) {
|
||||
ERROR_MSG("Requested to close the output file for the recording, but the output filename is empty; not supposed to happen. We're still going to close the file descriptor though.");
|
||||
}
|
||||
|
||||
if (close(outputFileDescriptor) < 0) {
|
||||
FAIL_MSG("Error: failed to close the output file: %s. We're resetting the file descriptor anyway.", strerror(errno));
|
||||
}
|
||||
|
||||
outputFileDescriptor = -1;
|
||||
|
||||
INFO_MSG("Close the file for the recording: %s", config->getString("outputfilename").c_str());
|
||||
|
||||
if (Triggers::shouldTrigger("RECORDING_STOP")) {
|
||||
|
||||
if (0 == config->getString("streamname").size()) {
|
||||
ERROR_MSG("Streamname is empty; the RECORDING_STOP trigger will not know what stream stopped it's recording. We do execute the trigger.");
|
||||
}
|
||||
|
||||
std::string payload;
|
||||
payload = config->getString("streamname") +"\n";
|
||||
payload += config->getString("outputfilename");
|
||||
|
||||
Triggers::doTrigger("RECORDING_STOP", payload, streamName.c_str());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
/*end-roxlu*/
|
||||
bool Output::recording(){
|
||||
if (config->getString("outputfilename").size() > 0) {
|
||||
DONTEVEN_MSG("We're not sending a HTTP response because we're currently creating a recording.");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -63,6 +63,10 @@ namespace Mist {
|
|||
long unsigned int getMainSelectedTrack();
|
||||
void updateMeta();
|
||||
void selectDefaultTracks();
|
||||
/*begin-roxlu*/
|
||||
bool openOutputFileForRecording(); // Opens the output file and uses dup2() to make sure that all stdout is written into a file.
|
||||
bool closeOutputFileForRecording(); // Closes the output file into which we're writing and resets the file descriptor.
|
||||
/*end-roxlu*/
|
||||
static bool listenMode(){return true;}
|
||||
//virtuals. The optional virtuals have default implementations that do as little as possible.
|
||||
virtual void sendNext() {}//REQUIRED! Others are optional.
|
||||
|
@ -76,6 +80,13 @@ namespace Mist {
|
|||
virtual void sendHeader();
|
||||
virtual void onFail();
|
||||
virtual void requestHandler();
|
||||
virtual void onRecord(){
|
||||
wantRequest = false;
|
||||
parseData = true;
|
||||
realTime = 1000;
|
||||
|
||||
seek(0);
|
||||
}
|
||||
private://these *should* not be messed with in child classes.
|
||||
/*LTS-START*/
|
||||
void Log(std::string type, std::string message);
|
||||
|
@ -122,6 +133,10 @@ namespace Mist {
|
|||
bool sentHeader;///< If false, triggers sendHeader if parseData is true.
|
||||
|
||||
std::map<int,DTSCPageData> bookKeeping;
|
||||
/*begin-roxlu*/
|
||||
int outputFileDescriptor; // Write output into this file.
|
||||
/*end-roxlu*/
|
||||
bool recording();
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -241,6 +241,7 @@ namespace Mist {
|
|||
for (std::set<unsigned long>::iterator it = toRemove.begin(); it != toRemove.end(); it++){
|
||||
selectedTracks.erase(*it);
|
||||
}
|
||||
|
||||
onHTTP();
|
||||
if (!H.bufferChunks){
|
||||
H.Clean();
|
||||
|
@ -396,4 +397,22 @@ namespace Mist {
|
|||
}
|
||||
/*LTS-END*/
|
||||
|
||||
|
||||
/*begin-roxlu*/
|
||||
void HTTPOutput::sendResponse(std::string message, std::string code) {
|
||||
|
||||
// Only send output when we're not creating a recording.
|
||||
if (recording()) return;
|
||||
|
||||
if (code.size() == 0) {
|
||||
WARN_MSG("Requested to send a HTTP response but the given code is empty. Trying though.");
|
||||
}
|
||||
|
||||
if (message.size() == 0) {
|
||||
WARN_MSG("Requested to send a HTTP response but the given message is empty. Trying though.");
|
||||
}
|
||||
|
||||
H.SendResponse(message, code, myConn);
|
||||
}
|
||||
/*end-roxlu*/
|
||||
}
|
||||
|
|
|
@ -14,9 +14,16 @@ namespace Mist {
|
|||
virtual void onFail();
|
||||
virtual void onHTTP(){};
|
||||
virtual void requestHandler();
|
||||
virtual void onRecord(){
|
||||
Output::onRecord();
|
||||
H.sendingChunks = false;
|
||||
}
|
||||
static bool listenMode(){return false;}
|
||||
void reConnector(std::string & connector);
|
||||
std::string getHandler();
|
||||
/*begin-roxlu*/
|
||||
void sendResponse(std::string message, std::string code = "200");
|
||||
/*end-roxlu*/
|
||||
protected:
|
||||
HTTP::Parser H;
|
||||
std::string getConnectedHost();//LTS
|
||||
|
|
|
@ -24,13 +24,13 @@ namespace Mist {
|
|||
capa["methods"][0u]["handler"] = "http";
|
||||
capa["methods"][0u]["type"] = "html5/video/mp2t";
|
||||
capa["methods"][0u]["priority"] = 1ll;
|
||||
capa["canRecord"].append("ts");
|
||||
}
|
||||
|
||||
void OutHTTPTS::onHTTP(){
|
||||
std::string method = H.method;
|
||||
|
||||
initialize();
|
||||
H.Clean();
|
||||
H.SetHeader("Content-Type", "video/mp2t");
|
||||
H.setCORSHeaders();
|
||||
if(method == "OPTIONS" || method == "HEAD"){
|
||||
|
@ -41,11 +41,12 @@ namespace Mist {
|
|||
H.StartResponse(H, myConn);
|
||||
parseData = true;
|
||||
wantRequest = false;
|
||||
H.Clean(); //clean for any possible next requests
|
||||
}
|
||||
|
||||
void OutHTTPTS::sendTS(const char * tsData, unsigned int len){
|
||||
//if (!recording()){
|
||||
H.Chunkify(tsData, len, myConn);
|
||||
//}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ namespace Mist {
|
|||
capa["methods"][0u]["type"] = "flash/7";
|
||||
capa["methods"][0u]["priority"] = 5ll;
|
||||
capa["methods"][0u]["player_url"] = "/oldflashplayer.swf";
|
||||
capa["canRecord"].append("flv");
|
||||
}
|
||||
|
||||
void OutProgressiveFLV::sendNext(){
|
||||
|
@ -43,7 +44,7 @@ namespace Mist {
|
|||
H.SetHeader("Content-Type", "video/x-flv");
|
||||
H.protocol = "HTTP/1.0";
|
||||
H.setCORSHeaders();
|
||||
H.SendResponse("200", "OK", myConn);
|
||||
sendResponse("OK");
|
||||
myConn.SendNow(FLV::Header, 13);
|
||||
tag.DTSCMetaInit(myMeta, selectedTracks);
|
||||
myConn.SendNow(tag.data, tag.len);
|
||||
|
|
|
@ -26,6 +26,9 @@ namespace Mist {
|
|||
capa["methods"][0u]["handler"] = "http";
|
||||
capa["methods"][0u]["type"] = "html5/video/mp4";
|
||||
capa["methods"][0u]["priority"] = 8ll;
|
||||
///\todo uncomment when we actually start implementing mp4 recording
|
||||
//capa["canRecord"].append("mp4");
|
||||
//capa["canRecord"].append("m3u");
|
||||
}
|
||||
|
||||
long long unsigned OutProgressiveMP4::estimateFileSize() {
|
||||
|
|
Loading…
Add table
Reference in a new issue