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
|
@ -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,8 +146,14 @@ 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
|
||||
static char liveSemName[NAME_BUFFER_SIZE];
|
||||
|
@ -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){
|
||||
H.Chunkify(tsData, len, myConn);
|
||||
//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
Add a link
Reference in a new issue