(M)JPG HTTP output support
This commit is contained in:
parent
ecd7e324dd
commit
eed71b66d4
10 changed files with 92 additions and 300 deletions
|
@ -32,8 +32,6 @@ embed_files = [
|
||||||
{'infile': '../embed/min/skins/default.css', 'variable': 'skin_default_css', 'outfile': 'skin_default.css.h'},
|
{'infile': '../embed/min/skins/default.css', 'variable': 'skin_default_css', 'outfile': 'skin_default.css.h'},
|
||||||
{'infile': '../embed/min/skins/dev.css', 'variable': 'skin_dev_css', 'outfile': 'skin_dev.css.h'},
|
{'infile': '../embed/min/skins/dev.css', 'variable': 'skin_dev_css', 'outfile': 'skin_dev.css.h'},
|
||||||
{'infile': '../embed/skins/video-js.css', 'variable': 'skin_videojs_css', 'outfile': 'skin_videojs.css.h'},
|
{'infile': '../embed/skins/video-js.css', 'variable': 'skin_videojs_css', 'outfile': 'skin_videojs.css.h'},
|
||||||
{'infile': '../src/output/noffmpeg.jpg', 'variable': 'noffmpeg', 'outfile': 'noffmpeg.h'},
|
|
||||||
{'infile': '../src/output/noh264.jpg', 'variable': 'noh264', 'outfile': 'noh264.h'},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
embed_tgts = []
|
embed_tgts = []
|
||||||
|
|
18
lib/util.cpp
18
lib/util.cpp
|
@ -316,6 +316,24 @@ namespace Util{
|
||||||
rndSrc.close();
|
rndSrc.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Secure random alphanumeric string generator
|
||||||
|
/// Uses getRandomBytes internally
|
||||||
|
std::string getRandomAlphanumeric(size_t len){
|
||||||
|
std::string ret(len, 'X');
|
||||||
|
getRandomBytes((void*)ret.data(), len);
|
||||||
|
for (size_t i = 0; i < len; ++i){
|
||||||
|
uint8_t v = (ret[i] % 62);
|
||||||
|
if (v < 10){
|
||||||
|
ret[i] = v + '0';
|
||||||
|
}else if (v < 36){
|
||||||
|
ret[i] = v - 10 + 'A';
|
||||||
|
}else{
|
||||||
|
ret[i] = v - 10 - 26 + 'a';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
/// 64-bits version of ftell
|
/// 64-bits version of ftell
|
||||||
uint64_t ftell(FILE *stream){
|
uint64_t ftell(FILE *stream){
|
||||||
/// \TODO Windows implementation (e.g. _ftelli64 ?)
|
/// \TODO Windows implementation (e.g. _ftelli64 ?)
|
||||||
|
|
|
@ -21,6 +21,7 @@ namespace Util{
|
||||||
int64_t expBackoffMs(const size_t currIter, const size_t maxIter, const int64_t maxWait);
|
int64_t expBackoffMs(const size_t currIter, const size_t maxIter, const int64_t maxWait);
|
||||||
|
|
||||||
void getRandomBytes(void * dest, size_t len);
|
void getRandomBytes(void * dest, size_t len);
|
||||||
|
std::string getRandomAlphanumeric(size_t len);
|
||||||
|
|
||||||
uint64_t ftell(FILE *stream);
|
uint64_t ftell(FILE *stream);
|
||||||
uint64_t fseek(FILE *stream, uint64_t offset, int whence);
|
uint64_t fseek(FILE *stream, uint64_t offset, int whence);
|
||||||
|
|
|
@ -21,7 +21,6 @@ option('DEBUG', description: 'Default debug level. Recommended value for develop
|
||||||
option('NOGA', description: 'Disables Google Analytics entirely in the LSP', type: 'boolean', value: false)
|
option('NOGA', description: 'Disables Google Analytics entirely in the LSP', type: 'boolean', value: false)
|
||||||
option('LOAD_BALANCE', description: 'Build the load balancer (WIP)', type: 'boolean', value: false)
|
option('LOAD_BALANCE', description: 'Build the load balancer (WIP)', type: 'boolean', value: false)
|
||||||
option('WITH_AV', description: 'Build a generic libav-based input (not distributable!)', type: 'boolean', value: false)
|
option('WITH_AV', description: 'Build a generic libav-based input (not distributable!)', type: 'boolean', value: false)
|
||||||
option('WITH_JPG', description: 'Build JPG thumbnailer output support (WIP)', type: 'boolean', value: false)
|
|
||||||
option('WITH_SANITY', description: 'Enable MistOutSanityCheck output for testing purposes', type: 'boolean', value: false)
|
option('WITH_SANITY', description: 'Enable MistOutSanityCheck output for testing purposes', type: 'boolean', value: false)
|
||||||
option('LSP_MINIFY', description: 'Try to minify LSP JS via java closure-compiler, generally not needed unless changing JS code as a minified version is part of the repository already', type: 'boolean', value: false)
|
option('LSP_MINIFY', description: 'Try to minify LSP JS via java closure-compiler, generally not needed unless changing JS code as a minified version is part of the repository already', type: 'boolean', value: false)
|
||||||
option('LOCAL_GENERATORS', description: 'Attempts to find a locally-installed version of sourcery and make_html, instead of compiling it', type: 'boolean', value: false)
|
option('LOCAL_GENERATORS', description: 'Attempts to find a locally-installed version of sourcery and make_html, instead of compiling it', type: 'boolean', value: false)
|
||||||
|
|
|
@ -381,6 +381,16 @@ namespace Mist{
|
||||||
trueCodec = "JPEG";
|
trueCodec = "JPEG";
|
||||||
trueType = "video";
|
trueType = "video";
|
||||||
}
|
}
|
||||||
|
if (codec == "V_MS/VFW/FOURCC"){
|
||||||
|
tmpElem = E.findChild(EBML::EID_CODECPRIVATE);
|
||||||
|
if (tmpElem){
|
||||||
|
std::string bitmapheader = tmpElem.getValStringUntrimmed();
|
||||||
|
if (bitmapheader.substr(16, 4) == "MJPG"){
|
||||||
|
trueCodec = "JPEG";
|
||||||
|
trueType = "video";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (codec == "A_PCM/FLOAT/IEEE"){
|
if (codec == "A_PCM/FLOAT/IEEE"){
|
||||||
trueCodec = "FLOAT";
|
trueCodec = "FLOAT";
|
||||||
trueType = "audio";
|
trueType = "audio";
|
||||||
|
|
|
@ -22,6 +22,7 @@ outputs = [
|
||||||
{'name' : 'SDP', 'format' : 'sdp', 'extra': ['http']},
|
{'name' : 'SDP', 'format' : 'sdp', 'extra': ['http']},
|
||||||
{'name' : 'HTTP', 'format' : 'http_internal', 'extra': ['http','embed']},
|
{'name' : 'HTTP', 'format' : 'http_internal', 'extra': ['http','embed']},
|
||||||
{'name' : 'JSONLine', 'format' : 'jsonline'},
|
{'name' : 'JSONLine', 'format' : 'jsonline'},
|
||||||
|
{'name' : 'JPG', 'format' : 'jpg', 'extra': ['http']},
|
||||||
]
|
]
|
||||||
|
|
||||||
if usessl
|
if usessl
|
||||||
|
@ -39,10 +40,6 @@ if have_srt
|
||||||
outputs += {'name' : 'TSSRT', 'format' : 'tssrt', 'extra': ['ts', 'debased', 'with_srt']}
|
outputs += {'name' : 'TSSRT', 'format' : 'tssrt', 'extra': ['ts', 'debased', 'with_srt']}
|
||||||
endif
|
endif
|
||||||
|
|
||||||
if get_option('WITH_JPG')
|
|
||||||
outputs += {'name' : 'JPG', 'format' : 'jpg', 'extra': ['http','embed']}
|
|
||||||
endif
|
|
||||||
|
|
||||||
if get_option('WITH_SANITY')
|
if get_option('WITH_SANITY')
|
||||||
outputs += {'name' : 'SanityCheck', 'format' : 'sanitycheck'}
|
outputs += {'name' : 'SanityCheck', 'format' : 'sanitycheck'}
|
||||||
endif
|
endif
|
||||||
|
|
|
@ -119,8 +119,6 @@ namespace Mist{
|
||||||
cfg->addOption("target", opt);
|
cfg->addOption("target", opt);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OutEBML::isRecording(){return config->getString("target").size();}
|
|
||||||
|
|
||||||
/// Calculates the size of a Cluster (contents only) and returns it.
|
/// Calculates the size of a Cluster (contents only) and returns it.
|
||||||
/// Bases the calculation on the currently selected tracks and the given start/end time for the
|
/// Bases the calculation on the currently selected tracks and the given start/end time for the
|
||||||
/// cluster.
|
/// cluster.
|
||||||
|
|
|
@ -19,7 +19,6 @@ namespace Mist{
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool isRecording();
|
|
||||||
std::string doctype;
|
std::string doctype;
|
||||||
void sendElemTrackEntry(size_t idx);
|
void sendElemTrackEntry(size_t idx);
|
||||||
size_t sizeElemTrackEntry(size_t idx);
|
size_t sizeElemTrackEntry(size_t idx);
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
#include "output_jpg.h"
|
#include "output_jpg.h"
|
||||||
#include <fstream>
|
|
||||||
#include <mist/bitfields.h>
|
#include <mist/bitfields.h>
|
||||||
#include <mist/mp4_generic.h>
|
#include <mist/mp4_generic.h>
|
||||||
#include <mist/procs.h>
|
#include <mist/procs.h>
|
||||||
|
@ -9,129 +8,76 @@
|
||||||
|
|
||||||
namespace Mist{
|
namespace Mist{
|
||||||
OutJPG::OutJPG(Socket::Connection &conn) : HTTPOutput(conn){
|
OutJPG::OutJPG(Socket::Connection &conn) : HTTPOutput(conn){
|
||||||
HTTP = false;
|
motion = false;
|
||||||
cachedir = config->getString("cachedir");
|
if (isRecording()){
|
||||||
if (cachedir.size()){
|
motion = (config->getString("target").find(".mj") != std::string::npos);
|
||||||
cachedir += "/MstJPEG" + streamName;
|
|
||||||
cachetime = config->getInteger("cachetime");
|
|
||||||
}else{
|
|
||||||
cachetime = 0;
|
|
||||||
}
|
}
|
||||||
if (config->getString("target").size()){
|
}
|
||||||
initialize();
|
|
||||||
if (!streamName.size()){
|
void OutJPG::respondHTTP(const HTTP::Parser & req, bool headersOnly){
|
||||||
WARN_MSG("Recording unconnected JPG output to file! Cancelled.");
|
// Set global defaults
|
||||||
conn.close();
|
HTTPOutput::respondHTTP(req, headersOnly);
|
||||||
return;
|
|
||||||
}
|
motion = (req.url.find(".mj") != std::string::npos);
|
||||||
if (!M){
|
if (motion){
|
||||||
INFO_MSG("Stream not available - aborting");
|
boundary = Util::getRandomAlphanumeric(24);
|
||||||
conn.close();
|
H.SetHeader("Content-Type", "multipart/x-mixed-replace;boundary="+boundary);
|
||||||
return;
|
H.SetHeader("Connection", "close");
|
||||||
}
|
}
|
||||||
if (!userSelect.size()){
|
|
||||||
INFO_MSG("Stream codec not supported - aborting");
|
H.CleanPreserveHeaders();
|
||||||
conn.close();
|
H.SendResponse("200", "OK", myConn);
|
||||||
return;
|
if (headersOnly){return;}
|
||||||
}
|
if (motion){
|
||||||
// We generate a thumbnail first, then output it if successful
|
myConn.SendNow("\r\n--" + boundary + "\r\nContent-Type: image/jpeg\r\n\r\n");
|
||||||
generate();
|
}
|
||||||
if (!jpg_buffer.str().size()){
|
parseData = true;
|
||||||
// On failure, report, but do not open the file or write anything
|
wantRequest = false;
|
||||||
FAIL_MSG("Could not generate thumbnail for %s", streamName.c_str());
|
}
|
||||||
myConn.close();
|
|
||||||
return;
|
void OutJPG::sendNext(){
|
||||||
}
|
char *dataPointer = 0;
|
||||||
if (config->getString("target") == "-"){
|
size_t len = 0;
|
||||||
INFO_MSG("Outputting %s to stdout in JPG format", streamName.c_str());
|
thisPacket.getString("data", dataPointer, len);
|
||||||
}else{
|
myConn.SendNow(dataPointer, len);
|
||||||
if (!connectToFile(config->getString("target"))){
|
if (!motion){
|
||||||
myConn.close();
|
Util::logExitReason(ER_CLEAN_EOF, "end of single JPG frame");
|
||||||
return;
|
|
||||||
}
|
|
||||||
INFO_MSG("Recording %s to %s in JPG format", streamName.c_str(), config->getString("target").c_str());
|
|
||||||
}
|
|
||||||
myConn.SendNow(jpg_buffer.str().c_str(), jpg_buffer.str().size());
|
|
||||||
myConn.close();
|
myConn.close();
|
||||||
return;
|
}else{
|
||||||
|
myConn.SendNow("\r\n--" + boundary + "\r\nContent-Type: image/jpeg\r\n\r\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pretends the stream is always ready to play - we don't care about waiting times or whatever
|
/// Pretends the stream is always ready to play - we don't care about waiting times or whatever
|
||||||
bool OutJPG::isReadyForPlay(){return true;}
|
bool OutJPG::isReadyForPlay(){return true;}
|
||||||
|
|
||||||
void OutJPG::initialSeek(bool dryRun){
|
|
||||||
size_t mainTrack = getMainSelectedTrack();
|
|
||||||
if (mainTrack == INVALID_TRACK_ID){return;}
|
|
||||||
INFO_MSG("Doing initial seek");
|
|
||||||
if (M.getLive()){
|
|
||||||
liveSeek();
|
|
||||||
uint32_t targetKey = M.getKeyIndexForTime(mainTrack, currentTime());
|
|
||||||
seek(M.getTimeForKeyIndex(mainTrack, targetKey));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// cancel if there are no keys in the main track
|
|
||||||
if (!M.getValidTracks().count(mainTrack) || !M.getLastms(mainTrack)){
|
|
||||||
WARN_MSG("Aborted vodSeek because no tracks selected");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint64_t seekPos = M.getFirstms(mainTrack) + (M.getLastms(mainTrack) - M.getFirstms(mainTrack)) / 2;
|
|
||||||
bool didSeek = false;
|
|
||||||
size_t retries = 10;
|
|
||||||
while (!didSeek && --retries){
|
|
||||||
MEDIUM_MSG("VoD seek to %" PRIu64 "ms", seekPos);
|
|
||||||
uint32_t targetKey = M.getKeyIndexForTime(mainTrack, seekPos);
|
|
||||||
didSeek = seek(M.getTimeForKeyIndex(mainTrack, targetKey));
|
|
||||||
if (!didSeek){
|
|
||||||
selectDefaultTracks();
|
|
||||||
mainTrack = getMainSelectedTrack();
|
|
||||||
}
|
|
||||||
seekPos = M.getFirstms(mainTrack) + (M.getLastms(mainTrack) - M.getFirstms(mainTrack)) * (((double)retries)/10.0);
|
|
||||||
}
|
|
||||||
if (!didSeek){
|
|
||||||
onFail("Could not seek to location for image");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OutJPG::init(Util::Config *cfg){
|
void OutJPG::init(Util::Config *cfg){
|
||||||
HTTPOutput::init(cfg);
|
HTTPOutput::init(cfg);
|
||||||
capa["name"] = "JPG";
|
capa["name"] = "JPG";
|
||||||
capa["desc"] = "Allows getting a representative key frame as JPG image. Requires ffmpeg (with "
|
capa["desc"] = "Support both single-frame JPEG and motion JPEG (e.g. MJPEG) over HTTP";
|
||||||
"h264 decoding and jpeg encoding) to be "
|
capa["url_rel"].append("/$.jpg");
|
||||||
"installed in the PATH.";
|
capa["url_rel"].append("/$.mjpg");
|
||||||
capa["url_rel"] = "/$.jpg";
|
capa["url_match"].append("/$.jpg");
|
||||||
capa["url_match"] = "/$.jpg";
|
capa["url_match"].append("/$.jpeg");
|
||||||
capa["codecs"][0u][0u].append("H264");
|
capa["url_match"].append("/$.mjpg");
|
||||||
|
capa["url_match"].append("/$.mjpeg");
|
||||||
|
capa["codecs"][0u][0u].append("JPEG");
|
||||||
capa["methods"][0u]["handler"] = "http";
|
capa["methods"][0u]["handler"] = "http";
|
||||||
capa["methods"][0u]["type"] = "html5/image/jpeg";
|
capa["methods"][0u]["type"] = "html5/image/jpeg";
|
||||||
capa["methods"][0u]["hrn"] = "JPEG";
|
capa["methods"][0u]["hrn"] = "JPEG image";
|
||||||
capa["methods"][0u]["priority"] = 0;
|
capa["methods"][0u]["url_rel"] = "/$.jpg";
|
||||||
|
capa["methods"][0u]["priority"] = 1;
|
||||||
|
capa["methods"][1u]["handler"] = "http";
|
||||||
|
capa["methods"][1u]["type"] = "html5/image/jpeg";
|
||||||
|
capa["methods"][1u]["hrn"] = "JPEG stream";
|
||||||
|
capa["methods"][1u]["url_rel"] = "/$.mjpg";
|
||||||
|
capa["methods"][1u]["priority"] = 2;
|
||||||
config->addStandardPushCapabilities(capa);
|
config->addStandardPushCapabilities(capa);
|
||||||
capa["push_urls"].append("/*.jpg");
|
capa["push_urls"].append("/*.jpg");
|
||||||
|
capa["push_urls"].append("/*.jpeg");
|
||||||
|
capa["push_urls"].append("/*.mjpg");
|
||||||
|
capa["push_urls"].append("/*.mjpeg");
|
||||||
|
|
||||||
capa["optional"]["cachedir"]["name"] = "Cache directory";
|
|
||||||
capa["optional"]["cachedir"]["help"] =
|
|
||||||
"Location to store cached images, preferably in RAM somewhere";
|
|
||||||
capa["optional"]["cachedir"]["option"] = "--cachedir";
|
|
||||||
capa["optional"]["cachedir"]["short"] = "D";
|
|
||||||
capa["optional"]["cachedir"]["default"] = "/tmp";
|
|
||||||
capa["optional"]["cachedir"]["type"] = "string";
|
|
||||||
capa["optional"]["cachetime"]["name"] = "Cache time";
|
|
||||||
capa["optional"]["cachetime"]["help"] =
|
|
||||||
"Duration in seconds to wait before refreshing cached images. Does not apply to VoD "
|
|
||||||
"streams (VoD is cached infinitely)";
|
|
||||||
capa["optional"]["cachetime"]["option"] = "--cachetime";
|
|
||||||
capa["optional"]["cachetime"]["short"] = "T";
|
|
||||||
capa["optional"]["cachetime"]["default"] = 30;
|
|
||||||
capa["optional"]["cachetime"]["type"] = "uint";
|
|
||||||
capa["optional"]["ffopts"]["name"] = "Ffmpeg arguments";
|
|
||||||
capa["optional"]["ffopts"]["help"] =
|
|
||||||
"Extra arguments to use when generating the jpg file through ffmpeg";
|
|
||||||
capa["optional"]["ffopts"]["option"] = "--ffopts";
|
|
||||||
capa["optional"]["ffopts"]["short"] = "F";
|
|
||||||
capa["optional"]["ffopts"]["default"] = "-qscale:v 4";
|
|
||||||
capa["optional"]["ffopts"]["type"] = "string";
|
|
||||||
cfg->addOptionsFromCapabilities(capa);
|
cfg->addOptionsFromCapabilities(capa);
|
||||||
|
|
||||||
JSON::Value opt;
|
JSON::Value opt;
|
||||||
|
@ -142,175 +88,4 @@ namespace Mist{
|
||||||
cfg->addOption("target", opt);
|
cfg->addOption("target", opt);
|
||||||
}
|
}
|
||||||
|
|
||||||
void OutJPG::onHTTP(){
|
|
||||||
std::string method = H.method;
|
|
||||||
H.clearHeader("Range");
|
|
||||||
H.clearHeader("Icy-MetaData");
|
|
||||||
H.clearHeader("User-Agent");
|
|
||||||
H.setCORSHeaders();
|
|
||||||
if (method == "OPTIONS" || method == "HEAD"){
|
|
||||||
H.SetHeader("Content-Type", "image/jpeg");
|
|
||||||
H.protocol = "HTTP/1.1";
|
|
||||||
H.SendResponse("200", "OK", myConn);
|
|
||||||
H.Clean();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
initialize();
|
|
||||||
if (!userSelect.size()){
|
|
||||||
H.protocol = "HTTP/1.0";
|
|
||||||
H.setCORSHeaders();
|
|
||||||
H.body.clear();
|
|
||||||
H.SendResponse("200", "Unprocessable: not H264", myConn);
|
|
||||||
#include "noh264.h"
|
|
||||||
myConn.SendNow(noh264, noh264_len);
|
|
||||||
myConn.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
H.SetHeader("Content-Type", "image/jpeg");
|
|
||||||
H.protocol = "HTTP/1.0";
|
|
||||||
H.setCORSHeaders();
|
|
||||||
H.StartResponse(H, myConn);
|
|
||||||
HTTP = true;
|
|
||||||
generate();
|
|
||||||
if (!jpg_buffer.str().size()){
|
|
||||||
NoFFMPEG();
|
|
||||||
}else{
|
|
||||||
H.Chunkify(jpg_buffer.str().c_str(), jpg_buffer.str().size(), myConn);
|
|
||||||
if (cachedir.size()){
|
|
||||||
std::ofstream cachefile;
|
|
||||||
cachefile.open(cachedir.c_str());
|
|
||||||
cachefile << jpg_buffer.str();
|
|
||||||
cachefile.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
H.Chunkify("", 0, myConn);
|
|
||||||
H.Clean();
|
|
||||||
HTTP = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OutJPG::NoFFMPEG(){
|
|
||||||
FAIL_MSG("Could not start ffmpeg! Is it installed on the system?");
|
|
||||||
#include "noffmpeg.h"
|
|
||||||
if (HTTP){
|
|
||||||
H.Chunkify(noffmpeg, noffmpeg_len, myConn);
|
|
||||||
}else{
|
|
||||||
myConn.SendNow(noffmpeg, noffmpeg_len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OutJPG::generate(){
|
|
||||||
// If we're caching, check if the cache hasn't expired yet...
|
|
||||||
if (cachedir.size() && cachetime){
|
|
||||||
struct stat statData;
|
|
||||||
if (stat(cachedir.c_str(), &statData) != -1){
|
|
||||||
if (Util::epoch() - statData.st_mtime <= cachetime || M.getVod()){
|
|
||||||
std::ifstream cachefile;
|
|
||||||
cachefile.open(cachedir.c_str());
|
|
||||||
char buffer[8 * 1024];
|
|
||||||
while (cachefile.good() && myConn){
|
|
||||||
cachefile.read(buffer, 8 * 1024);
|
|
||||||
uint32_t s = cachefile.gcount();
|
|
||||||
if (HTTP){
|
|
||||||
H.Chunkify(buffer, s, myConn);
|
|
||||||
}else{
|
|
||||||
myConn.SendNow(buffer, s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cachefile.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initialSeek();
|
|
||||||
size_t mainTrack = getMainSelectedTrack();
|
|
||||||
if (mainTrack == INVALID_TRACK_ID){
|
|
||||||
FAIL_MSG("Could not select valid track");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int fin = -1, fout = -1, ferr = 2;
|
|
||||||
pid_t ffmpeg = -1;
|
|
||||||
// Start ffmpeg quietly if we're < MEDIUM debug level
|
|
||||||
char ffcmd[256];
|
|
||||||
ffcmd[255] = 0; // ensure there is an ending null byte
|
|
||||||
snprintf(ffcmd, 255, "ffmpeg %s -f h264 -i - %s -vframes 1 -f mjpeg -",
|
|
||||||
(Util::printDebugLevel >= DLVL_MEDIUM ? "" : "-v quiet"),
|
|
||||||
config->getString("ffopts").c_str());
|
|
||||||
|
|
||||||
HIGH_MSG("Starting JPG command: %s", ffcmd);
|
|
||||||
char *args[128];
|
|
||||||
uint8_t argCnt = 0;
|
|
||||||
char *startCh = 0;
|
|
||||||
for (char *i = ffcmd; i - ffcmd < 256; ++i){
|
|
||||||
if (!*i){
|
|
||||||
if (startCh){args[argCnt++] = startCh;}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (*i == ' '){
|
|
||||||
if (startCh){
|
|
||||||
args[argCnt++] = startCh;
|
|
||||||
startCh = 0;
|
|
||||||
*i = 0;
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
if (!startCh){startCh = i;}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
args[argCnt] = 0;
|
|
||||||
|
|
||||||
ffmpeg = Util::Procs::StartPiped(args, &fin, &fout, &ferr);
|
|
||||||
if (ffmpeg < 2){
|
|
||||||
Socket::Connection failure(fin, fout);
|
|
||||||
failure.close();
|
|
||||||
NoFFMPEG();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
VERYHIGH_MSG("Started ffmpeg, PID %" PRIu64 ", pipe %" PRIu32 "/%" PRIu32, (uint64_t)ffmpeg,
|
|
||||||
(uint32_t)fin, (uint32_t)fout);
|
|
||||||
Socket::Connection ffconn(fin, -1);
|
|
||||||
|
|
||||||
// Send H264 init data in Annex B format
|
|
||||||
MP4::AVCC avccbox;
|
|
||||||
avccbox.setPayload(M.getInit(mainTrack));
|
|
||||||
ffconn.SendNow(avccbox.asAnnexB());
|
|
||||||
INSANE_MSG("Sent init data to ffmpeg...");
|
|
||||||
|
|
||||||
if (ffconn && prepareNext() && thisPacket){
|
|
||||||
uint64_t keytime = thisPacket.getTime();
|
|
||||||
do{
|
|
||||||
char *p = 0;
|
|
||||||
size_t l = 0;
|
|
||||||
uint32_t o = 0;
|
|
||||||
thisPacket.getString("data", p, l);
|
|
||||||
// Send all NAL units in the key frame, in Annex B format
|
|
||||||
while (o + 4 < l){
|
|
||||||
// get NAL unit size
|
|
||||||
uint32_t s = Bit::btohl(p + o);
|
|
||||||
// make sure we don't go out of bounds of packet
|
|
||||||
if (o + s + 4 > l){break;}
|
|
||||||
// Send H264 Annex B start code
|
|
||||||
ffconn.SendNow("\000\000\000\001", 4);
|
|
||||||
// Send NAL unit
|
|
||||||
ffconn.SendNow(p + o + 4, s);
|
|
||||||
INSANE_MSG("Sent h264 %" PRIu32 "b NAL unit to ffmpeg (time: %" PRIu64 ")...", s,
|
|
||||||
thisPacket.getTime());
|
|
||||||
// Skip to next NAL unit
|
|
||||||
o += s + 4;
|
|
||||||
}
|
|
||||||
INSANE_MSG("Sent whole packet, checking next...");
|
|
||||||
}while (ffconn && prepareNext() && thisPacket && thisPacket.getTime() == keytime);
|
|
||||||
}
|
|
||||||
ffconn.close();
|
|
||||||
// Output ffmpeg result data to socket
|
|
||||||
jpg_buffer.clear();
|
|
||||||
Socket::Connection ffout(-1, fout);
|
|
||||||
while (myConn && ffout && (ffout.spool() || ffout.Received().size())){
|
|
||||||
while (myConn && ffout.Received().size()){
|
|
||||||
jpg_buffer << ffout.Received().get();
|
|
||||||
ffout.Received().get().clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ffout.close();
|
|
||||||
}
|
|
||||||
}// namespace Mist
|
}// namespace Mist
|
||||||
|
|
|
@ -1,23 +1,20 @@
|
||||||
#include "output_http.h"
|
#include "output_http.h"
|
||||||
#include <sstream>
|
|
||||||
|
|
||||||
namespace Mist{
|
namespace Mist{
|
||||||
class OutJPG : public HTTPOutput{
|
class OutJPG : public HTTPOutput{
|
||||||
public:
|
public:
|
||||||
OutJPG(Socket::Connection &conn);
|
OutJPG(Socket::Connection &conn);
|
||||||
static void init(Util::Config *cfg);
|
static void init(Util::Config *cfg);
|
||||||
void onHTTP();
|
void respondHTTP(const HTTP::Parser & req, bool headersOnly);
|
||||||
|
void sendNext();
|
||||||
bool isReadyForPlay();
|
bool isReadyForPlay();
|
||||||
|
protected:
|
||||||
private:
|
virtual bool isFileTarget(){return isRecording();}
|
||||||
void generate();
|
virtual bool inlineRestartCapable() const{return true;}
|
||||||
void initialSeek(bool dryRun = false);
|
bool motion;
|
||||||
void NoFFMPEG();
|
std::string boundary;
|
||||||
std::string cachedir;
|
|
||||||
uint64_t cachetime;
|
|
||||||
bool HTTP;
|
|
||||||
std::stringstream jpg_buffer;
|
|
||||||
};
|
};
|
||||||
}// namespace Mist
|
}// namespace Mist
|
||||||
|
|
||||||
typedef Mist::OutJPG mistOut;
|
typedef Mist::OutJPG mistOut;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue