├── .gitignore ├── HLS-Stream-Creator.sh ├── LICENSE ├── README.md ├── helpers.js ├── hls-ffmpeg.js ├── package.json └── prueba.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test.mov 3 | test.mp4 4 | output/* 5 | !output/readme.md 6 | .DS_Store -------------------------------------------------------------------------------- /HLS-Stream-Creator.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # A very simple BASH script to take an input video and split it down into Segments 4 | # before creating an M3U8 Playlist, allowing the file to be served using HLS 5 | # 6 | # 7 | 8 | ###################################################################################### 9 | # 10 | # Copyright (c) 2013, Ben Tasker 11 | # All rights reserved. 12 | # 13 | # Redistribution and use in source and binary forms, with or without modification, 14 | # are permitted provided that the following conditions are met: 15 | # 16 | # Redistributions of source code must retain the above copyright notice, this 17 | # list of conditions and the following disclaimer. 18 | # 19 | # Redistributions in binary form must reproduce the above copyright notice, this 20 | # list of conditions and the following disclaimer in the documentation and/or 21 | # other materials provided with the distribution. 22 | # 23 | # Neither the name of Ben Tasker nor the names of his 24 | # contributors may be used to endorse or promote products derived from 25 | # this software without specific prior written permission. 26 | # 27 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 28 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 29 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 30 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 31 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 32 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 33 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 34 | # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 35 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 36 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 37 | # 38 | ###################################################################################### 39 | 40 | # Basic config 41 | OUTPUT_DIRECTORY=${OUTPUT_DIRECTORY:-'./output'} 42 | 43 | # Change this if you want to specify a path to use a specific version of FFMPeg 44 | FFMPEG=${FFMPEG:-'ffmpeg'} 45 | 46 | # Number of threads which will be used for transcoding. With newer FFMPEGs and x264 47 | # encoders "0" means "optimal". This is normally the number of CPU cores. 48 | NUMTHREADS=${NUMTHREADS:-"0"} 49 | 50 | # Video codec for the output video. Will be used as an value for the -vcodec argument 51 | VIDEO_CODEC=${VIDEO_CODEC:-"libx264"} 52 | 53 | # Video codec for the output video. Will be used as an value for the -acodec argument 54 | AUDIO_CODEC=${AUDIO_CODEC:-"libfdk_aac"} 55 | 56 | # Additional flags for ffmpeg 57 | FFMPEG_FLAGS=${FFMPEG_FLAGS:-""} 58 | 59 | # If the input is a live stream (i.e. linear video) this should be 1 60 | LIVE_STREAM=${LIVE_STREAM:-0} 61 | 62 | # Video bitrates to use in output (comma seperated list if you want to create an adaptive stream.) 63 | # leave null to use the input bitrate 64 | OP_BITRATES=${OP_BITRATES:-''} 65 | 66 | # Determines whether the processing for adaptive streams should run sequentially or not 67 | NO_FORK=${NO_FORK:-0} 68 | 69 | # Lets put our functions here 70 | 71 | 72 | ## Output the script's CLI Usage 73 | # 74 | # 75 | function print_usage(){ 76 | 77 | cat << EOM 78 | HTTP Live Stream Creator 79 | Version 1 80 | 81 | Copyright (C) 2013 B Tasker, D Atanasov 82 | Released under BSD 3 Clause License 83 | See LICENSE 84 | 85 | 86 | Usage: HLS-Stream-Creator.sh -[lf] [-c segmentcount] -i [inputfile] -s [segmentlength(seconds)] -o [outputdir] -b [bitrates] [-p filename] 87 | 88 | -i Input file 89 | -s Segment length (seconds) 90 | -o Output directory (default: ./output) 91 | -l Input is a live stream 92 | -c Number of segments to include in playlist (live streams only) - 0 is no limit 93 | -b Output video Bitrates (comma seperated list for adaptive streams) 94 | -f Foreground encoding only (don't fork the encoding processes into the background - adaptive non-live streams only) 95 | -p Playlist filename 96 | -t Segment filename prefix 97 | -S Segment directory name (default none) 98 | -e Encrypt the HLS segments (default none) 99 | 100 | Deprecated Legacy usage: 101 | HLS-Stream-Creator.sh inputfile segmentlength(seconds) [outputdir='./output'] 102 | 103 | EOM 104 | 105 | exit 106 | 107 | } 108 | 109 | 110 | function createStream(){ 111 | # For VoD and single bitrate streams the variables we need will exist in Global scope. 112 | # for live adaptive streams though, that won't be the case, so we need to take them as arguments 113 | # Some are global though, so we'll leave those as is 114 | 115 | playlist_name="$1" 116 | output_name="$2" 117 | bitrate="$3" 118 | infile="$4" 119 | 120 | $FFMPEG -i "$infile" \ 121 | -loglevel error -y \ 122 | -vcodec "$VIDEO_CODEC" \ 123 | -acodec "$AUDIO_CODEC" \ 124 | -threads "$NUMTHREADS" \ 125 | -map 0 \ 126 | -flags \ 127 | -global_header \ 128 | -f segment \ 129 | -segment_list "$playlist_name" \ 130 | -segment_time "$SEGLENGTH" \ 131 | -segment_format mpeg_ts \ 132 | -sn \ 133 | $bitrate \ 134 | $FFMPEG_ADDITIONAL \ 135 | $FFMPEG_FLAGS \ 136 | "$OUTPUT_DIRECTORY/$output_name" 137 | } 138 | 139 | 140 | function createVariantPlaylist(){ 141 | playlist_name="$1" 142 | echo "#EXTM3U" > "$playlist_name" 143 | } 144 | 145 | 146 | function appendVariantPlaylistentry(){ 147 | playlist_name=$1 148 | playlist_path=$2 149 | playlist_bw=$(( $3 * 1000 )) # bits not bytes :) 150 | 151 | cat << EOM >> "$playlist_name" 152 | #EXT-X-STREAM-INF:BANDWIDTH=$playlist_bw 153 | $playlist_path 154 | EOM 155 | 156 | } 157 | 158 | 159 | function awaitCompletion(){ 160 | # Monitor the encoding pids for their completion status 161 | while [ ${#PIDS[@]} -ne 0 ]; do 162 | # Calculate the length of the array 163 | pid_length=$((${#PIDS[@]} - 1)) 164 | 165 | # Check each PID in the array 166 | for i in `seq 0 $pid_length` 167 | do 168 | # Test whether the pid is still active 169 | if ! kill -0 ${PIDS[$i]} 2> /dev/null 170 | then 171 | echo "Encoding for bitrate ${BITRATE_PROCESSES[$i]}k completed" 172 | 173 | if [ "$LIVE_STREAM" == "1" ] && [ `grep 'EXT-X-ENDLIST' "$OUTPUT_DIRECTORY/${PLAYLIST_PREFIX}_${BITRATE_PROCESSES[$i]}.m3u8" | wc -l ` == "0" ] 174 | then 175 | # Correctly terminate the manifest. See HLS-15 for info on why 176 | echo "#EXT-X-ENDLIST" >> "$OUTPUT_DIRECTORY/${PLAYLIST_PREFIX}_${BITRATE_PROCESSES[$i]}.m3u8" 177 | fi 178 | 179 | unset BITRATE_PROCESSES[$i] 180 | unset PIDS[$i] 181 | fi 182 | done 183 | PIDS=("${PIDS[@]}") # remove any nulls 184 | sleep 1 185 | done 186 | } 187 | 188 | function encrypt(){ 189 | # Encrypt the generated segments with AES-128 bits 190 | 191 | KEY_FILE="$OUTPUT_DIRECTORY/${PLAYLIST_PREFIX}.key" 192 | 193 | openssl rand 16 > $KEY_FILE 194 | ENCRYPTION_KEY=$(cat $KEY_FILE | hexdump -e '16/1 "%02x"') 195 | 196 | count=0 197 | for file in $(ls ${OUTPUT_DIRECTORY}/*.ts | cut -f3 -d '/') 198 | do 199 | ENC_FILENAME="$OUTPUT_DIRECTORY/${SEGMENT_PREFIX}_enc_${count}".ts 200 | 201 | INIT_VECTOR=$(printf '%032x' $count) 202 | openssl aes-128-cbc -e -in $OUTPUT_DIRECTORY/$file -out $ENC_FILENAME -nosalt -iv $INIT_VECTOR -K $ENCRYPTION_KEY 203 | 204 | # Move encrypted file to the original filename, so that the m3u8 file does not have to be changed 205 | mv $ENC_FILENAME ${OUTPUT_DIRECTORY}/$file 206 | 207 | count=$((count+1)) 208 | done 209 | 210 | # Insert the KEY at the 5'th line in the m3u8 file 211 | sed -i "5i #EXT-X-KEY:METHOD=AES-128,URI="${PLAYLIST_PREFIX}.key "$OUTPUT_DIRECTORY/${PLAYLIST_PREFIX}.m3u8" 212 | } 213 | 214 | # This is used internally, if the user wants to specify their own flags they should be 215 | # setting FFMPEG_FLAGS 216 | FFMPEG_ADDITIONAL='' 217 | LIVE_SEGMENT_COUNT=0 218 | IS_FIFO=0 219 | TMPDIR=${TMPDIR:-"/tmp"} 220 | MYPID=$$ 221 | # Get the input data 222 | 223 | # This exists to maintain b/c 224 | LEGACY_ARGS=1 225 | 226 | # If even one argument is supplied, switch off legacy argument style 227 | while getopts "i:o:s:c:b:p:t:S:lfe" flag 228 | do 229 | LEGACY_ARGS=0 230 | case "$flag" in 231 | i) INPUTFILE="$OPTARG";; 232 | o) OUTPUT_DIRECTORY="$OPTARG";; 233 | s) SEGLENGTH="$OPTARG";; 234 | l) LIVE_STREAM=1;; 235 | c) LIVE_SEGMENT_COUNT="$OPTARG";; 236 | b) OP_BITRATES="$OPTARG";; 237 | f) NO_FORK=1;; 238 | p) PLAYLIST_PREFIX="$OPTARG";; 239 | t) SEGMENT_PREFIX="$OPTARG";; 240 | S) SEGMENT_DIRECTORY="$OPTARG";; 241 | e) ENCRYPT=1;; 242 | esac 243 | done 244 | 245 | 246 | if [ "$LEGACY_ARGS" == "1" ] 247 | then 248 | # Old Basic Usage is 249 | # cmd.sh inputfile segmentlength 250 | 251 | INPUTFILE=${INPUTFILE:-$1} 252 | SEGLENGTH=${SEGLENGTH:-$2} 253 | OUTPUT_DIRECTORY=$3 254 | if ! [ -z "$3" ] 255 | then 256 | OUTPUT_DIRECTORY=$3 257 | fi 258 | fi 259 | 260 | 261 | # Check we've got the arguments we need 262 | if [ "$INPUTFILE" == "" ] || [ "$SEGLENGTH" == "" ] 263 | then 264 | print_usage 265 | fi 266 | 267 | # FFMpeg is a pre-requisite, so let check for it 268 | if hash $FFMPEG 2> /dev/null 269 | then 270 | # FFMpeg exists 271 | echo "ffmpeg command found.... continuing" 272 | else 273 | # FFMPeg doesn't exist, uh-oh! 274 | echo "Error: FFmpeg doesn't appear to exist in your PATH. Please addresss and try again" 275 | exit 1 276 | fi 277 | 278 | # Check whether the input is a named pipe 279 | if [ -p "$INPUTFILE" ] 280 | then 281 | echo "Warning: Input is FIFO - EXPERIMENTAL" 282 | IS_FIFO=1 283 | 284 | fi 285 | 286 | # Check output directory exists otherwise create it 287 | if [ ! -w $OUTPUT_DIRECTORY ] 288 | then 289 | echo "Creating $OUTPUT_DIRECTORY" 290 | mkdir -p $OUTPUT_DIRECTORY 291 | fi 292 | 293 | if [ "$LIVE_STREAM" == "1" ] 294 | then 295 | FFMPEG_ADDITIONAL+="-segment_list_flags +live" 296 | 297 | if [ "$LIVE_SEGMENT_COUNT" -gt 0 ] 298 | then 299 | WRAP_POINT=$(($LIVE_SEGMENT_COUNT * 2)) # Wrap the segment numbering after 2 manifest lengths - prevents disks from filling 300 | FFMPEG_ADDITIONAL+=" -segment_list_size $LIVE_SEGMENT_COUNT -segment_wrap $WRAP_POINT" 301 | fi 302 | fi 303 | 304 | 305 | # Pulls file name from INPUTFILE which may be an absolute or relative path. 306 | INPUTFILENAME=${INPUTFILE##*/} 307 | 308 | # If a prefix hasn't been specified, use the input filename 309 | PLAYLIST_PREFIX=${PLAYLIST_PREFIX:-$INPUTFILENAME} 310 | SEGMENT_PREFIX=${SEGMENT_PREFIX:-$PLAYLIST_PREFIX} 311 | 312 | # The 'S' option allows segments and bitrate specific manifests to be placed in a subdir 313 | SEGMENT_DIRECTORY=${SEGMENT_DIRECTORY:-''} 314 | 315 | if [ ! "$SEGMENT_DIRECTORY" == "" ] 316 | then 317 | 318 | if [ ! -d "${OUTPUT_DIRECTORY}/${SEGMENT_DIRECTORY}" ] 319 | then 320 | mkdir "${OUTPUT_DIRECTORY}/${SEGMENT_DIRECTORY}" 321 | fi 322 | 323 | SEGMENT_DIRECTORY+="/" 324 | fi 325 | 326 | # Set the bitrate 327 | if [ ! "$OP_BITRATES" == "" ] 328 | then 329 | # Make the bitrate list easier to parse 330 | OP_BITRATES=${OP_BITRATES//,/$'\n'} 331 | 332 | # Create an array to house the pids for backgrounded tasks 333 | declare -a PIDS 334 | declare -a BITRATE_PROCESSES 335 | 336 | # Get the variant playlist created 337 | createVariantPlaylist "$OUTPUT_DIRECTORY/${PLAYLIST_PREFIX}_master.m3u8" 338 | for br in $OP_BITRATES 339 | do 340 | appendVariantPlaylistentry "$OUTPUT_DIRECTORY/${PLAYLIST_PREFIX}_master.m3u8" "${SEGMENT_DIRECTORY}${PLAYLIST_PREFIX}_${br}.m3u8" "$br" 341 | done 342 | 343 | OUTPUT_DIRECTORY+=$SEGMENT_DIRECTORY 344 | 345 | # Now for the longer running bit, transcode the video 346 | for br in $OP_BITRATES 347 | do 348 | BITRATE="-b:v ${br}k" 349 | # Finally, lets build the output filename format 350 | OUT_NAME="${SEGMENT_PREFIX}_${br}_%05d.ts" 351 | PLAYLIST_NAME="$OUTPUT_DIRECTORY/${PLAYLIST_PREFIX}_${br}.m3u8" 352 | SOURCE_FILE="$INPUTFILE" 353 | echo "Generating HLS segments for bitrate ${br}k - this may take some time" 354 | 355 | if [ "$NO_FORK" == "0" ] || [ "$LIVE_STREAM" == "1" ] 356 | then 357 | # Processing Starts 358 | if [ "$IS_FIFO" == "1" ] 359 | then 360 | # Create a FIFO specially for this bitrate 361 | SOURCE_FILE="$TMPDIR/hlsc.encode.$MYPID.$br" 362 | mknod "$SOURCE_FILE" p 363 | fi 364 | 365 | # Schedule the encode 366 | createStream "$PLAYLIST_NAME" "$OUT_NAME" "$BITRATE" "$SOURCE_FILE" & 367 | PID=$! 368 | PIDS=(${PIDS[@]} $PID) 369 | BITRATE_PROCESSES=(${BITRATE_PROCESSES[@]} $br) 370 | else 371 | createStream "$PLAYLIST_NAME" "$OUT_NAME" "$BITRATE" "$SOURCE_FILE" 372 | fi 373 | 374 | done 375 | 376 | if [ "$IS_FIFO" == "1" ] 377 | then 378 | # If the input was a FIFO we need to read from it and push into the new FIFOs 379 | cat "$INPUTFILE" | tee $(for br in $OP_BITRATES; do echo "$TMPDIR/hlsc.encode.$MYPID.$br"; done) > /dev/null & 380 | TEE_PID=$! 381 | fi 382 | 383 | if [ "$NO_FORK" == "0" ] || [ "$LIVE_STREAM" == "1" ] 384 | then 385 | # Monitor the background tasks for completion 386 | echo "All transcoding processes started, awaiting completion" 387 | awaitCompletion 388 | fi 389 | 390 | if [ "$IS_FIFO" == "1" ] 391 | then 392 | for br in $OP_BITRATES 393 | do 394 | rm -f "$TMPDIR/hlsc.encode.$MYPID.$br"; 395 | done 396 | # If we were interrupted, tee may still be running 397 | kill $TEE_PID 2> /dev/null 398 | fi 399 | 400 | else 401 | 402 | OUTPUT_DIRECTORY+=$SEGMENT_DIRECTORY 403 | # No bitrate specified 404 | 405 | # Finally, lets build the output filename format 406 | OUT_NAME="%02d.ts" 407 | PLAYLIST_NAME="$OUTPUT_DIRECTORY/pl.m3u8" 408 | 409 | echo "Generating HLS segments - this may take some time" 410 | 411 | # Processing Starts 412 | 413 | createStream "$PLAYLIST_NAME" "$OUT_NAME" "$BITRATE" "$INPUTFILE" 414 | 415 | 416 | if [ "$ENCRYPT" == "1" ] && [ "$LIVE_STREAM" == "0" ] 417 | then 418 | encrypt 419 | fi 420 | fi 421 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Anthony gavilan vinces 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hls-ffmpeg 2 | > Quick converter hls and ffmpeg resolutions 3 | 4 | ## Install 5 | 6 | ```js 7 | npm install --save hls-ffmpeg 8 | ``` 9 | 10 | ## Convert video 11 | 12 | ```js 13 | var hf = require('hls-ffmpeg') 14 | var json = { 15 | input: './360.mp4', 16 | format: '848x480', 17 | output: './output/test.mp4' 18 | } 19 | hf.ffmpeg(json1, function(err, data){ 20 | console.log(err||data) 21 | }) 22 | ``` 23 | ## Convert video 24 | 25 | ```js 26 | var json = { 27 | input: 'test.mov', 28 | time: '10' 29 | } 30 | 31 | hf.hls(json, function(err, data){ 32 | console.log(err||data); 33 | }) 34 | ``` -------------------------------------------------------------------------------- /helpers.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @description Converts an object without nested object properties e.g. {'-key1': 'String1', '-key2': 'String2'} into an array like ['-key1','-String1','-key2','-String2']. If you need a key twice put the values into an array 3 | * @example 4 | * objectToArray({'-vpre': ['slow', 'baseline'], '-vcodec': 'libx264'}) 5 | * returns ['-vpre','slow','-vpre','baseline','-vcodec','libx264'] 6 | * @param {Object} obj 7 | * @returns {Array} arr 8 | */ 9 | 10 | var objectToArray = exports.objectToArray = function (obj) { 11 | var arr = [], key; 12 | 13 | for (key in obj) { 14 | var objectValue = obj[key]; 15 | if(objectValue instanceof Array) { 16 | objectValue.forEach(function (elm, i) { 17 | arr.push(key, elm); 18 | }); 19 | } else { 20 | arr.push(key, objectValue); 21 | } 22 | } 23 | 24 | return arr; 25 | }; -------------------------------------------------------------------------------- /hls-ffmpeg.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module to drive ffmpeg video enconding library with shortcuts 4 | * for web video. Requires a ffmpeg compiled with support for mp4/ogg/webm. 5 | */ 6 | var path = require('path'), 7 | _ = require('lodash'), 8 | child_process = require('child_process'), 9 | spawn = child_process.spawn, 10 | helpers = require('./helpers.js'), 11 | that = this, 12 | queue = exports.queue = [], 13 | maxActive = 10, // Maximum of active jobs 14 | active = 0; 15 | 16 | 17 | 18 | function push (job) { 19 | queue.push(job); 20 | if(active < maxActive) { 21 | next(); 22 | } 23 | } 24 | 25 | function next () { 26 | if(queue.length > 0 && active < maxActive) { 27 | that.exec(queue[0].bin, queue[0].params, queue[0].callback); 28 | active++; 29 | queue.shift(); 30 | } 31 | } 32 | 33 | var formats = [{ 34 | quality: '360p', 35 | format: '640x360' 36 | },{ 37 | quality: '480p', 38 | format: '848x480' 39 | },{ 40 | quality: '720p', 41 | format: '1280x720' 42 | },{ 43 | quality: '1080p', 44 | format: '1920x1080' 45 | }] 46 | 47 | /** 48 | * Description: 49 | * calls ffmpeg or hls with the specified flags and returns the output 50 | * to the callback function. 51 | * 52 | * Parameters: 53 | * bin - a string for execute 'ffmpeg' or './HLS-Stream-Creator.sh' 54 | * params - an object of ffmpeg options, ex: {'-i': './test.3gp', '-vpre': ['slow', 'baseline'], '-vcodec': 'libx264'} 55 | * callback - a function to call when ffmpeg is done, ex: 56 | * function (stderr, stdout, exitCode) { ... } 57 | */ 58 | 59 | exports.exec = function (bin, params, callback) { 60 | if (params instanceof Array && params.length > 1) { 61 | var stderr = '', stdout = '', 62 | objeto = spawn(bin, params); 63 | objeto.stderr.on('data', function (err) { 64 | stderr += err; 65 | }); 66 | 67 | objeto.stdout.on('data', function (output) { 68 | stdout += output; 69 | }); 70 | 71 | objeto.on('exit', function (code) { 72 | callback(stderr, stdout, code); 73 | active--; 74 | next(); 75 | }); 76 | 77 | } else { 78 | if (params instanceof Array && params.length <= 2) { 79 | console.log('Params to short for ejecucion'); 80 | active--; 81 | next(); 82 | } 83 | } 84 | }; 85 | 86 | /** 87 | * Description: 88 | * Convert a multiples resolution 89 | * avoid code repetition. 90 | * 91 | * Parameters: 92 | * input - path/to/the/inputFile.ext as a string. 93 | * params - an object of ffmpeg options to be added to the predefined ones (optional). 94 | * output - path/to/the/outputFile.ext as a string (optional). 95 | * callback - function to call when ffmpeg is done, ex: 96 | * function (stderr, stdout, exitCode) { ... } 97 | */ 98 | 99 | 100 | exports.ffmpeg = function (json, callback) { 101 | if(Object.keys(json).length < 3) 102 | return callback('Falta Paramatros') 103 | var crf = json.crf || '14'; 104 | var threads = json.threads || '0'; 105 | 106 | var params = "-i " + json.input + " -vcodec libx264 -crf " + crf + 107 | " -threads " + threads + " -s " + json.format + " -acodec libfdk_aac -y " + json.output + 108 | " -sn"; 109 | if(json.thumbnail) 110 | params = params + " -s 640x360 -ss 00:00:01.00 -vframes 1 -y " + json.thumbnail; 111 | 112 | if(json.splash) 113 | params = params + " -ss 00:00:01.00 -vframes 1 -y " + json.splash; 114 | 115 | params = params.split(" "); 116 | push({bin: 'ffmpeg', params: params, callback: callback}); 117 | }; 118 | 119 | /** 120 | * Description: 121 | * Convert a video in hls 122 | * avoid code repetition. 123 | * 124 | * Parameters: 125 | * input - path/to/the/inputFile.ext as a string. 126 | * time - Segment length (seconds) 127 | * callback - function to call when ffmpeg is done, ex: 128 | * function (stderr, stdout, exitCode) { ... } 129 | */ 130 | 131 | exports.hls = function(json, callback){ 132 | if(Object.keys(json).length < 2) 133 | return callback('Falta Paramatros') 134 | var params = {}; 135 | 136 | params = helpers.objectToArray(_.assign({ 137 | '-i': json.input, 138 | '-s': json.time, 139 | '-o': json.output 140 | },params)); 141 | 142 | push({bin: './HLS-Stream-Creator.sh', params: params, callback: callback}); 143 | } 144 | 145 | 146 | /* 147 | * The function takes a absolute file path and returns a meta data object 148 | * @params {String} input 149 | * @params {Callback} callback function(error, jsonMetaObject) 150 | */ 151 | 152 | exports.getMetadata = function (input, callback) { 153 | child_process.exec("ffprobe -loglevel error -show_format -show_streams " + input + " -print_format json", function (error, stdout, stderr) { 154 | try { 155 | stdout = JSON.parse(stdout); 156 | callback(error, stdout); 157 | } catch (e) { 158 | callback(error, stdout); 159 | } 160 | }); 161 | }; 162 | 163 | exports.getLimitFormats = function (input, callback) { 164 | this.getMetadata(input, function(error, data){ 165 | var height = data.streams[0].coded_height 166 | 167 | var index = height >= 1080 ? 3 : height >= 720 ? 2 : height >= 480 ? 1 : 0 168 | 169 | var newFormats = formats.filter(function(format, i){ 170 | return i <= index 171 | }) 172 | 173 | callback(error, newFormats) 174 | }) 175 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hls-ffmpeg", 3 | "version": "0.0.9", 4 | "description": "Quick converter hls and ffmpeg resolutions", 5 | "main": "hls-ffmpeg", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/ghondar/hls-ffmpeg.git" 12 | }, 13 | "keywords": [ 14 | "hls", 15 | "ffmpeg", 16 | "video", 17 | "rtmp" 18 | ], 19 | "author": "Ghondar ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/ghondar/hls-ffmpeg/issues" 23 | }, 24 | "homepage": "https://github.com/ghondar/hls-ffmpeg", 25 | "dependencies": { 26 | "fs-extra": "^0.23.1", 27 | "lodash": "^3.10.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /prueba.js: -------------------------------------------------------------------------------- 1 | var hf = require('./hls-ffmpeg'); 2 | var path = require('path'); 3 | var fse = require('fs-extra'); 4 | var fs = require('fs'); 5 | 6 | var output = path.join(__dirname, 'output', 'prueba') 7 | 8 | var formats = [{ 9 | input: 'videoAhora.mov', 10 | ext: 'mov', 11 | quality: '360p', 12 | format: '640x360' 13 | },{ 14 | input: 'videoAhora.mov', 15 | ext: 'mov', 16 | quality: '480p', 17 | format: '848x480' 18 | },{ 19 | input: 'videoAhora.mov', 20 | ext: 'mov', 21 | quality: '720p', 22 | format: '1280x720' 23 | },{ 24 | input: 'videoAhora.mov', 25 | ext: 'mov', 26 | quality: '1080p', 27 | format: '1920x1080' 28 | }]; 29 | 30 | var count = 0; 31 | var hlsCount = 0; 32 | 33 | formats.forEach(function(format){ 34 | format.name = format.input.split('.'+format.ext)[0]; 35 | var pathVideo = path.join(output, format.name); 36 | var m3u8 = path.join(pathVideo, format.name + '.m3u8'); 37 | var bandwidth = ''; 38 | switch(format.quality){ 39 | case '360p': 40 | bandwidth = '1000000'; 41 | break; 42 | case '480p': 43 | bandwidth = '1466589'; 44 | break; 45 | case '720p': 46 | bandwidth = '2000000' 47 | break; 48 | case '1080p': 49 | bandwidth = '3000000' 50 | break; 51 | } 52 | fse.mkdirsSync(pathVideo); 53 | if(count == 0){ 54 | format.splash = 'imagen.png' 55 | format.thumbnail = 'imagen-thumbnail.png' 56 | format.output = path.join(pathVideo, format.name +'.mp4'); 57 | hf.ffmpeg(format, function(err, data){ 58 | // console.log(err||data) 59 | console.log('historial', 1) 60 | var json = { 61 | input: format.output, 62 | time: '10', 63 | output: path.join(pathVideo, format.quality) 64 | } 65 | hf.hls(json, function(err, data){ 66 | // console.log(err||data); 67 | console.log('historial', 2) 68 | append({ 69 | path: m3u8, 70 | pl: path.join(format.quality, 'pl.m3u8'), 71 | bandwidth: bandwidth, 72 | format: format.format 73 | }) 74 | hlsCount ++; 75 | if(hlsCount === formats.length){ 76 | console.log('historial', 'ultimo') 77 | } 78 | if(count === formats.length) 79 | count = 0; 80 | }) 81 | }) 82 | }else{ 83 | format.output = path.join(pathVideo, format.name + '-'+ format.quality +'.mp4'); 84 | hf.ffmpeg(format, function(err, data){ 85 | // console.log(err||data) 86 | console.log('historial', 3) 87 | var json = { 88 | input: format.output, 89 | time: '10', 90 | output: path.join(pathVideo, format.quality) 91 | } 92 | hf.hls(json, function(err, data){ 93 | // console.log(err||data); 94 | console.log('historial', 4) 95 | append({ 96 | path: m3u8, 97 | pl: path.join(format.quality, 'pl.m3u8'), 98 | bandwidth: bandwidth, 99 | format: format.format 100 | }); 101 | hlsCount ++; 102 | if(hlsCount === formats.length){ 103 | console.log('historial', 'ultimo') 104 | } 105 | if(count === formats.length) 106 | count = 0; 107 | }) 108 | }) 109 | } 110 | count++; 111 | }) 112 | 113 | function append(json){ 114 | var text = '#EXT-X-STREAM-INF:BANDWIDTH=' + json.bandwidth + ',RESOLUTION=' + json.format + ',CODECS="avc1.4d401f,mp4a.40.5"\n'; 115 | text += json.pl + '\n'; 116 | if(!fs.existsSync(json.path)){ 117 | var prefix = '#EXTM3U\n'; 118 | prefix += text; 119 | fs.writeFileSync(json.path, prefix); 120 | }else 121 | fs.appendFileSync(json.path, text); 122 | } 123 | --------------------------------------------------------------------------------