(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
		Add a link
		
	
		Reference in a new issue