├── .gitignore ├── .npmignore ├── README.md ├── TODO.md ├── examples ├── mediainfo.js └── transcode.js ├── lib ├── transcoding.js └── transcoding │ ├── binding.js │ └── profiles.js ├── package.json ├── src ├── binding.cpp ├── hls │ ├── playlist.cpp │ └── playlist.h ├── io │ ├── filereader.cpp │ ├── filereader.h │ ├── filewriter.cpp │ ├── filewriter.h │ ├── io.cpp │ ├── io.h │ ├── iohandle.cpp │ ├── iohandle.h │ ├── nullwriter.cpp │ ├── nullwriter.h │ ├── streamreader.cpp │ ├── streamreader.h │ ├── streamwriter.cpp │ └── streamwriter.h ├── mediainfo.cpp ├── mediainfo.h ├── packetfifo.cpp ├── packetfifo.h ├── profile.cpp ├── profile.h ├── query.cpp ├── query.h ├── querycontext.cpp ├── querycontext.h ├── task.cpp ├── task.h ├── taskcontext.cpp ├── taskcontext.h ├── taskoptions.cpp ├── taskoptions.h └── utils.h └── wscript /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | binding.node 4 | .lock-wscript 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .gitignore 3 | .git/ 4 | .git* 5 | build 6 | binding.node 7 | .lock-wscript 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-transcoding -- Media transcoding and streaming for node.js 2 | ==================================== 3 | 4 | node-transcoding is a library for enabling both offline and real-time media 5 | transcoding. In addition to enabling the manipulation of the input media, 6 | utilities are provided to ease serving of the output. 7 | 8 | Currently supported features: 9 | 10 | * Nothing! 11 | 12 | Coming soon (maybe): 13 | 14 | * Everything! 15 | 16 | ## Quickstart 17 | 18 | npm install transcoding 19 | node 20 | > var transcoding = require('transcoding'); 21 | > transcoding.process('input.flv', 'output.m4v', 22 | transcoding.profiles.APPLE_TV_2, function(err, sourceInfo, targetInfo) { 23 | console.log('completed!'); 24 | }); 25 | 26 | ## Installation 27 | 28 | With [npm](http://npmjs.org): 29 | 30 | npm install transcoding 31 | 32 | From source: 33 | 34 | cd ~ 35 | git clone https://benvanik@github.com/benvanik/node-transcoding.git 36 | npm link node-transcoding/ 37 | 38 | ### Dependencies 39 | 40 | node-transcoding requires `ffmpeg` and its libraries `avformat` and `avcodec`. 41 | Make sure it's installed and on your path. It must be compiled with libx264 to 42 | support most output - note that some distributions don't include this and you 43 | may have to compile it yourself. Annoying, I know. 44 | 45 | #### Source 46 | 47 | ./configure \ 48 | --enable-gpl --enable-nonfree --enable-pthreads \ 49 | --enable-libfaac --enable-libfaad --enable-libmp3lame \ 50 | --enable-libx264 51 | sudo make install 52 | 53 | #### Mac OS X 54 | 55 | The easiest way to get ffmpeg is via [MacPorts](http://macports.org). 56 | Install it if needed and run the following from the command line: 57 | 58 | sudo port install ffmpeg +gpl +lame +x264 +xvid 59 | 60 | You may also need to add the MacPorts paths to your `~./profile`: 61 | 62 | export C_INCLUDE_PATH=$C_INCLUDE_PATH:/opt/local/include/ 63 | export CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/opt/local/include/ 64 | export LIBRARY_PATH=$LIBRARY_PATH:/opt/local/lib/ 65 | 66 | #### FreeBSD 67 | 68 | sudo pkg_add ffmpeg 69 | 70 | #### Linux 71 | 72 | # HAHA YEAH RIGHT GOOD LUCK >_> 73 | sudo apt-get install ffmpeg 74 | 75 | ## API 76 | 77 | ### Sources/targets 78 | 79 | All APIs take a source and target. Sources can be either strings representing 80 | file paths or Node readable streams. Targets can be either strings representing 81 | file paths or Node writable streams. Note that if possible you should use file 82 | paths, as it enables much faster access to the underlying data (~2x speed 83 | increase). 84 | 85 | ### Media Information 86 | 87 | Whenever 'info' is used, it refers to a MediaInfo object that looks something 88 | like this: 89 | 90 | { 91 | container: 'flv', 92 | duration: 126, // seconds 93 | start: 0, // seconds 94 | bitrate: 818000, // bits/sec 95 | streams: [ 96 | { 97 | type: 'video', 98 | codec: 'h264', 99 | profile: 'Main', 100 | profileId: 77, 101 | profileLevel: 30, 102 | resolution: { width: 640, height: 360 }, 103 | bitrate: 686000, 104 | fps: 29.97 105 | }, { 106 | type: 'audio', 107 | language: 'eng', 108 | codec: 'aac', 109 | sampleRate: 44100, // Hz 110 | channels: 2, 111 | bitrate: 131000 112 | } 113 | ] 114 | } 115 | 116 | Note that many of these fields are optional, such as bitrate, language, profile 117 | information, and even fps/duration. Don't go using the values without checking 118 | for undefined first. 119 | 120 | ### Querying Media Information 121 | 122 | To quickly query media information (duration, codecs used, etc) use the 123 | `queryInfo` API: 124 | 125 | var transcoding = require('transcoding'); 126 | transcoding.queryInfo(source, function(err, info) { 127 | // Completed 128 | }); 129 | 130 | ### Transcoding Profiles 131 | 132 | Transcoding requires a ton of parameters to get the best results. It's a pain in 133 | the ass. So what's exposed right now is a profile set that tries to set the 134 | best options for you. Pick your profile and pass it into the transcoding APIs. 135 | 136 | var transcoding = require('transcoding'); 137 | for (var profileName in transcoding.profiles) { 138 | var profile = transcoding.profiles[profileName]; 139 | console.log(profileName + ':' + util.inspect(profile)); 140 | } 141 | 142 | ### Simple Transcoding 143 | 144 | If you are doing simple offline transcoding (no need for streaming, extra 145 | options, progress updates, etc) then you can use the `process` API: 146 | 147 | var transcoding = require('transcoding'); 148 | transcoding.process(source, target, transcoding.profiles.APPLE_TV_2, {}, function(err, sourceInfo, targetInfo) { 149 | // Completed 150 | }); 151 | 152 | Note that this effectively just wraps the advanced API, without the need to 153 | track events. 154 | 155 | ### Advanced Transcoding 156 | 157 | var transcoding = require('transcoding'); 158 | var task = transcoding.createTask(source, target, transcoding.profiles.APPLE_TV_2); 159 | 160 | task.on('begin', function(sourceInfo, targetInfo) { 161 | // Transcoding beginning, info available 162 | console.log('transcoding beginning...'); 163 | console.log('source:'); 164 | console.log(util.inspect(sourceInfo)); 165 | console.log('target:'); 166 | console.log(util.inspect(targetInfo)); 167 | }); 168 | task.on('progress', function(progress) { 169 | // New progress made, currrently at timestamp out of duration 170 | // progress = { 171 | // timestamp: 0, // current seconds timestamp in the media 172 | // duration: 0, // total seconds in the media 173 | // timeElapsed: 0, // seconds elapsed so far 174 | // timeEstimated: 0, // seconds estimated for total task 175 | // timeRemaining: 0, // seconds remaining until done 176 | // timeMultiplier: 2 // multiples of real time the transcoding is 177 | // // occuring in (2 = 2x media time) 178 | // } 179 | console.log(util.inspect(progress)); 180 | console.log('progress ' + (progress.timestamp / progress.duration) + '%'); 181 | }); 182 | task.on('error', function(err) { 183 | // Error occurred, transcoding ending 184 | console.log('error: ' + err); 185 | }); 186 | task.on('end', function() { 187 | // Transcoding has completed 188 | console.log('finished'); 189 | }); 190 | 191 | // Start transcoding 192 | task.start(); 193 | 194 | // At any time, abort transcoding 195 | task.stop(); 196 | 197 | ### HTTP Live Streaming 198 | 199 | * [IETF Spec](http://tools.ietf.org/html/draft-pantos-http-live-streaming-07) 200 | * [Apple Docs](http://developer.apple.com/library/ios/#documentation/networkinginternet/conceptual/streamingmediaguide/Introduction/Introduction.html) 201 | 202 | If you are targeting devices that support HTTP Live Streaming (like iOS), you 203 | can have the transcoder build the output in realtime as it processes. This 204 | enables playback while the transcoding is occuring, as well as some other fancy 205 | things such as client-side stream switching (changing audio channels/etc). 206 | 207 | var task = transcoding.createTask(source, null, profile, { 208 | liveStreaming: { 209 | path: '/some/path/', 210 | name: 'base_name', 211 | segmentDuration: 10, 212 | allowCaching: true 213 | } 214 | }); 215 | 216 | This will result in a playlist file and the MPEGTS segments being placed under 217 | `/some/path/` with the name `base_name.m3u8`. The playlist file will be 218 | automatically updated as new segments are generated, and files can be assumed to 219 | be static once they are available. 220 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ## Remuxing 2 | 3 | * Figure out why audio often gets garbled, video playback rates differ, etc 4 | 5 | ## Transcoding 6 | 7 | * Codec management (decoder/encoder/etc) 8 | * Downscaling/etc filter options (->720p, etc) 9 | 10 | ## HTTP Live Streaming 11 | 12 | Support HTTP Live Streaming for live transcoding output to iOS/etc devices. 13 | Should generate the main playlist (and keep it updated) as well as manage all 14 | of the segment generation/writing/etc. 15 | 16 | * HLSWriter IOHandle type for streaming info (manifest file, path, etc) 17 | * Rework TaskContext to either support segmenting or subclass it 18 | 19 | // path/name.m3u8 20 | // /name.0.ts 21 | // /name.1.ts 22 | -------------------------------------------------------------------------------- /examples/mediainfo.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var transcoding = require('transcoding'); 6 | var util = require('util'); 7 | 8 | var opts = require('tav').set({ 9 | }); 10 | 11 | if (opts.args.length < 1) { 12 | console.log('no input file specified'); 13 | return; 14 | } 15 | 16 | var inputFile = path.normalize(opts.args[0]); 17 | if (!path.existsSync(inputFile)) { 18 | console.log('input file not found'); 19 | return; 20 | } 21 | 22 | transcoding.queryInfo(inputFile, function(err, info) { 23 | console.log('Info for ' + inputFile + ':'); 24 | if (err) { 25 | console.log('Error!'); 26 | console.log(err); 27 | } else { 28 | console.log(util.inspect(info, false, 3)); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /examples/transcode.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'); 4 | var http = require('http'); 5 | var path = require('path'); 6 | var transcoding = require('transcoding'); 7 | var url = require('url'); 8 | var util = require('util'); 9 | 10 | var opts = require('tav').set({ 11 | profile: { 12 | note: 'Encoding profile', 13 | value: 'APPLE_TV_2' 14 | }, 15 | stream_input: { 16 | note: 'Stream file input to test streaming', 17 | value: false 18 | }, 19 | stream_output: { 20 | note: 'Stream file output to test streaming', 21 | value: false 22 | }, 23 | livestreaming: { 24 | note: 'Enable HTTP Live Streaming output', 25 | value: false 26 | }, 27 | segmentDuration: { 28 | note: 'HTTP Live Streaming segment duration', 29 | value: 10 30 | } 31 | }); 32 | 33 | if (!opts.args.length) { 34 | console.log('All profiles:'); 35 | for (var key in transcoding.profiles) { 36 | var profile = transcoding.profiles[key]; 37 | console.log(' ' + key + ': ' + profile.name); 38 | } 39 | return; 40 | } 41 | 42 | if (opts.args.length < 1) { 43 | console.log('no input file specified'); 44 | return; 45 | } 46 | 47 | // Should really rewrite all of this flow... 48 | // HTTP requests are 'deferred' and will call process() themselves - others 49 | // will want it called after all this code completes 50 | var deferredRequest = false; 51 | var process = opts.args.length < 2 ? processQuery : processTranscode; 52 | 53 | var inputFile = opts.args[0]; 54 | var source; 55 | if (inputFile == '-') { 56 | // STDIN 57 | source = process.stdin; 58 | } else if (inputFile == 'null') { 59 | source = null; 60 | } else if (inputFile.indexOf('http') == 0) { 61 | // Web request 62 | var sourceUrl = url.parse(inputFile); 63 | var headers = {}; 64 | headers['Accept'] = '*/*'; 65 | headers['Accept-Charset'] = 'ISO-8859-1,utf-8;q=0.7,*;q=0.3'; 66 | headers['Accept-Encoding'] = 'identity;q=1, *;q=0'; 67 | headers['Accept-Language'] = 'en-US,en;q=0.8'; 68 | headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) ' + 69 | 'AppleWebKit/535.8 (KHTML, like Gecko) Chrome/17.0.930.0 Safari/535.8'; 70 | var req = http.request({ 71 | hostname: sourceUrl.hostname, 72 | port: sourceUrl.port, 73 | method: 'GET', 74 | path: sourceUrl.path, 75 | headers: headers 76 | }, function(res) { 77 | source = res; 78 | process(source, target); 79 | }); 80 | req.end(); 81 | deferredRequest = true; 82 | } else { 83 | // Local file 84 | inputFile = path.normalize(inputFile); 85 | if (!path.existsSync(inputFile)) { 86 | console.log('input file not found'); 87 | return; 88 | } 89 | source = inputFile; 90 | 91 | // Test files via streams 92 | if (opts['stream_input']) { 93 | source = fs.createReadStream(inputFile); 94 | } 95 | } 96 | 97 | var transcodeOptions = { 98 | }; 99 | 100 | var target; 101 | if (opts.args.length >= 2) { 102 | var profile = transcoding.profiles[opts['profile']]; 103 | if (!profile) { 104 | console.log('unknown profile: ' + profile); 105 | return; 106 | } 107 | 108 | var outputFile = opts.args[1]; 109 | if (opts['livestreaming']) { 110 | // Must be a path 111 | var outputPath = path.dirname(outputFile); 112 | if (!path.existsSync(outputPath)) { 113 | fs.mkdirSync(outputPath); 114 | } 115 | target = null; 116 | transcodeOptions.liveStreaming = { 117 | path: outputPath, 118 | name: path.basename(outputFile), 119 | segmentDuration: parseInt(opts['segmentDuration']), 120 | allowCaching: true 121 | }; 122 | } else if (outputFile == '-') { 123 | // STDOUT 124 | target = process.stdout; 125 | } else if (outputFile == 'null') { 126 | target = null; 127 | } else if (outputFile.indexOf('http') == 0) { 128 | // TODO: setup server for streaming? 129 | console.log('not yet implemented: HTTP serving'); 130 | return; 131 | } else { 132 | // Local file 133 | outputFile = path.normalize(outputFile); 134 | var outputPath = path.dirname(outputFile); 135 | if (!path.existsSync(outputPath)) { 136 | fs.mkdirSync(outputPath); 137 | } 138 | target = outputFile; 139 | 140 | // Test files via streams 141 | if (opts['stream_output']) { 142 | target = fs.createWriteStream(outputFile); 143 | } 144 | } 145 | 146 | console.log('transcoding ' + inputFile + ' -> ' + outputFile); 147 | } else { 148 | console.log('querying info on ' + inputFile); 149 | } 150 | 151 | function processQuery(source) { 152 | transcoding.queryInfo(source, function(err, info) { 153 | console.log('Info for ' + inputFile + ':'); 154 | console.log(util.inspect(info, false, 3)); 155 | }); 156 | }; 157 | 158 | function processTranscode(source, target) { 159 | var task = transcoding.createTask(source, target, profile, transcodeOptions); 160 | task.on('begin', function(sourceInfo, targetInfo) { 161 | // Transcoding beginning 162 | console.log('transcoding beginning...'); 163 | console.log('source:'); 164 | console.log(util.inspect(sourceInfo, false, 3)); 165 | console.log('target:'); 166 | console.log(util.inspect(targetInfo, false, 3)); 167 | }); 168 | task.on('progress', function(progress) { 169 | // New progress made, currrently at timestamp out of duration 170 | //console.log(util.inspect(progress)); 171 | console.log('progress ' + 172 | (progress.timestamp / progress.duration * 100) + '%'); 173 | }); 174 | task.on('error', function(err) { 175 | // Error occurred 176 | console.log('error: ' + err); 177 | }); 178 | task.on('end', function() { 179 | // Transcoding has completed 180 | console.log('finished'); 181 | }); 182 | task.start(); 183 | }; 184 | 185 | if (!deferredRequest) { 186 | process(source, target); 187 | } 188 | -------------------------------------------------------------------------------- /lib/transcoding.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var binding = require('./transcoding/binding'); 4 | 5 | exports.profiles = require('./transcoding/profiles'); 6 | 7 | // Enable extra FFMPEG debug spew 8 | binding.setDebugLevel(true); 9 | 10 | exports.StreamType = { 11 | AUDIO: 'audio', 12 | VIDEO: 'video', 13 | SUBTITLE: 'subtitle' 14 | }; 15 | 16 | exports.queryInfo = function(source, callback) { 17 | var query = new binding.Query(source); 18 | query.on('info', function(sourceInfo) { 19 | callback(undefined, sourceInfo); 20 | }); 21 | query.on('error', function(err) { 22 | callback(err, undefined); 23 | }); 24 | query.start(); 25 | }; 26 | 27 | exports.createTask = function(source, target, profile, opt_options) { 28 | return new binding.Task(source, target, profile, opt_options); 29 | }; 30 | 31 | exports.process = function(source, target, profile, opt_options, callback) { 32 | var sourceInfoStash = null; 33 | var targetInfoStash = null; 34 | var task = new binding.Task(source, target, profile, opt_options); 35 | task.on('begin', function(sourceInfo, targetInfo) { 36 | sourceInfoStash = sourceInfo; 37 | targetInfoStash = targetInfo; 38 | }); 39 | task.on('error', function(err) { 40 | callback(err, undefined, undefined); 41 | }); 42 | task.on('end', function() { 43 | callback(undefined, sourceInfoStash, targetInfoStash); 44 | }); 45 | task.start(); 46 | }; 47 | -------------------------------------------------------------------------------- /lib/transcoding/binding.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../build/Release/node_transcoding.node'); 2 | 3 | // WEAK: required in v0.6 4 | function inheritEventEmitter(type) { 5 | function extend(target, source) { 6 | for (var k in source.prototype) { 7 | target.prototype[k] = source.prototype[k]; 8 | } 9 | } 10 | var events = require('events'); 11 | extend(type, events.EventEmitter); 12 | } 13 | inheritEventEmitter(module.exports.Query); 14 | inheritEventEmitter(module.exports.Task); 15 | -------------------------------------------------------------------------------- /lib/transcoding/profiles.js: -------------------------------------------------------------------------------- 1 | // NOTE: each profile can have multiple video or audio configurations - the 2 | // best match based on source input will be used, or default to the first 3 | // defined. For example, if the profile defines both aac and mp3 audio, if the 4 | // source contains either it will be copied instead of transcoded. 5 | 6 | var video_h264_low = { 7 | codec: 'h264', 8 | profileId: 77, 9 | profileLevel: 30, 10 | bitrate: 600000 11 | }; 12 | var video_h264_high = { 13 | codec: 'h264', 14 | profileId: 77, 15 | profileLevel: 30, 16 | bitrate: 4500000 17 | }; 18 | 19 | var audio_aac_low = { 20 | codec: 'aac', 21 | channels: 2, 22 | sampleRate: 44100, 23 | sampleFormat: 's16', 24 | bitrate: 40000 25 | }; 26 | var audio_aac_high = { 27 | codec: 'aac', 28 | channels: 2, 29 | sampleRate: 44100, 30 | sampleFormat: 's16', 31 | bitrate: 128000 32 | }; 33 | 34 | var audio_mp3_low = { 35 | codec: 'mp3', 36 | channels: 2, 37 | sampleRate: 44100, 38 | sampleFormat: 's16', 39 | bitrate: 40000 40 | }; 41 | var audio_mp3_high = { 42 | codec: 'mp3', 43 | channels: 2, 44 | sampleRate: 44100, 45 | sampleFormat: 's16', 46 | bitrate: 128000 47 | }; 48 | 49 | function declareProfile(key, name, options) { 50 | var profile = { 51 | name: name, 52 | options: options 53 | }; 54 | exports[key] = profile; 55 | } 56 | 57 | declareProfile('APPLE_IOS', 'Apple iPhone/iPad', { 58 | container: 'mov', 59 | video: video_h264_low, 60 | audio: [audio_aac_low, audio_mp3_low] 61 | }); 62 | 63 | declareProfile('APPLE_TV_2', 'Apple TV 2', { 64 | container: 'mov', 65 | video: video_h264_high, 66 | audio: [audio_aac_high, audio_mp3_high] 67 | }); 68 | 69 | declareProfile('PLAYSTATION_3', 'Playstation 3', { 70 | 71 | }); 72 | 73 | declareProfile('XBOX_360', 'Xbox 360', { 74 | 75 | }); 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transcoding", 3 | "description": "Media transcoding and streaming support", 4 | "version": "0.0.1", 5 | "author": "Ben Vanik ", 6 | "contributors": [], 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/benvanik/node-transcoding.git" 10 | }, 11 | "keywords": [ 12 | "audio", 13 | "video", 14 | "media", 15 | "transcode", 16 | "transcoding", 17 | "remux", 18 | "streaming" 19 | ], 20 | "directories": { 21 | "lib": "./lib/transcoding" 22 | }, 23 | "main": "./lib/transcoding", 24 | "bin": { 25 | "transcode" : "./examples/transcode.js" 26 | }, 27 | "dependencies": { 28 | "tav": "0.1.0" 29 | }, 30 | "scripts": { 31 | "preinstall": "node-waf configure && node-waf build", 32 | "preuninstall": "rm -rf build/*" 33 | }, 34 | "engines": { 35 | "node": ">= 0.6.x" 36 | }, 37 | "devDependencies": {} 38 | } 39 | -------------------------------------------------------------------------------- /src/binding.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "utils.h" 4 | #include "query.h" 5 | #include "task.h" 6 | #include "io/io.h" 7 | 8 | using namespace transcoding; 9 | using namespace transcoding::io; 10 | 11 | namespace transcoding { 12 | 13 | static Handle setDebugLevel(const Arguments& args) { 14 | HandleScope scope; 15 | 16 | Local debugLog = args[0]->ToBoolean(); 17 | if (debugLog->Value()) { 18 | av_log_set_level(AV_LOG_DEBUG); 19 | } else { 20 | av_log_set_level(AV_LOG_QUIET); 21 | } 22 | 23 | return scope.Close(Undefined()); 24 | } 25 | 26 | }; // transcoding 27 | 28 | extern "C" void node_transcoding_init(Handle target) { 29 | HandleScope scope; 30 | 31 | // One-time prep 32 | av_register_all(); 33 | av_log_set_level(AV_LOG_QUIET); 34 | 35 | transcoding::Query::Init(target); 36 | transcoding::Task::Init(target); 37 | 38 | NODE_SET_METHOD(target, "setDebugLevel", transcoding::setDebugLevel); 39 | } 40 | 41 | NODE_MODULE(node_transcoding, node_transcoding_init); 42 | -------------------------------------------------------------------------------- /src/hls/playlist.cpp: -------------------------------------------------------------------------------- 1 | #include "playlist.h" 2 | 3 | using namespace std; 4 | using namespace transcoding; 5 | using namespace transcoding::hls; 6 | 7 | // TODO: use a new protocol version to get floating point numbers for durations 8 | // v3+ allow them in EXTINF 9 | 10 | Playlist::Playlist(string& path, string& name, 11 | double segmentDuration, bool allowCache) : 12 | path(path), name(name), segmentDuration(segmentDuration) { 13 | TC_LOG_D("Playlist::Playlist(%s, %s, %d, %s)\n", 14 | path.c_str(), name.c_str(), (int)(segmentDuration + 0.5), 15 | allowCache ? "cache" : "no-cache"); 16 | 17 | this->playlistFile = this->path + this->name + ".m3u8"; 18 | 19 | char str[1024]; 20 | sprintf(str, 21 | "#EXTM3U\n" 22 | "#EXT-X-VERSION:1\n" 23 | "#EXT-X-PLAYLIST-TYPE:EVENT\n" 24 | "#EXT-X-TARGETDURATION:%d\n" 25 | "#EXT-X-MEDIA-SEQUENCE:0\n" 26 | "#EXT-X-ALLOW-CACHE:%s\n", 27 | (int)(segmentDuration + 0.5), allowCache ? "YES" : "NO"); 28 | this->AppendString(str, false); 29 | } 30 | 31 | Playlist::~Playlist() { 32 | TC_LOG_D("Playlist::~Playlist()\n"); 33 | } 34 | 35 | string Playlist::GetSegmentPath(int id) { 36 | char name[64]; 37 | sprintf(name, "-%d.ts", id); 38 | return this->path + this->name + name; 39 | } 40 | 41 | int Playlist::AppendString(const char* str, bool append) { 42 | int r = 0; 43 | 44 | int flags = O_WRONLY | O_EXLOCK; 45 | if (append) { 46 | flags |= O_APPEND; 47 | } else { 48 | flags |= O_CREAT | O_TRUNC; 49 | } 50 | 51 | bool opened = false; 52 | uv_fs_t openReq; 53 | r = uv_fs_open(uv_default_loop(), 54 | &openReq, this->playlistFile.c_str(), 55 | flags, S_IWRITE | S_IREAD, NULL); 56 | assert(r != -1); 57 | if (!r) { 58 | opened = true; 59 | } 60 | 61 | if (!r) { 62 | uv_fs_t writeReq; 63 | r = uv_fs_write(uv_default_loop(), 64 | &writeReq, openReq.result, 65 | (void*)str, strlen(str), -1, NULL); 66 | assert(r != -1); 67 | } 68 | 69 | if (!r) { 70 | // NOTE: don't do just an fdatasync, as servers need valid modification 71 | // times as well as data 72 | // TODO: required? close may already be enough 73 | uv_fs_t syncReq; 74 | r = uv_fs_fsync(uv_default_loop(), 75 | &syncReq, openReq.result, NULL); 76 | assert(r != -1); 77 | } 78 | 79 | if (opened) { 80 | uv_fs_t closeReq; 81 | r = uv_fs_close(uv_default_loop(), 82 | &closeReq, openReq.result, NULL); 83 | assert(r != -1); 84 | } 85 | 86 | return r; 87 | } 88 | 89 | int Playlist::AddSegment(int id, double duration) { 90 | TC_LOG_D("Playlist::AddSegment(%d)\n", id); 91 | 92 | char str[1024]; 93 | sprintf(str, "#EXTINF:%d,\n%s-%d.ts\n", 94 | (int)(duration + 0.5), this->name.c_str(), id); 95 | return this->AppendString(str); 96 | } 97 | 98 | int Playlist::Complete() { 99 | TC_LOG_D("Playlist::Complete()\n"); 100 | 101 | return this->AppendString("#EXT-X-ENDLIST\n"); 102 | } 103 | -------------------------------------------------------------------------------- /src/hls/playlist.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "../utils.h" 4 | 5 | #ifndef NODE_TRANSCODING_HLS_PLAYLIST 6 | #define NODE_TRANSCODING_HLS_PLAYLIST 7 | 8 | using namespace v8; 9 | 10 | namespace transcoding { 11 | namespace hls { 12 | 13 | class Playlist { 14 | public: 15 | Playlist(std::string& path, std::string& name, double segmentDuration, 16 | bool allowCache); 17 | ~Playlist(); 18 | 19 | std::string GetSegmentPath(int id); 20 | 21 | int AddSegment(int id, double duration); 22 | int Complete(); 23 | 24 | private: 25 | int AppendString(const char* str, bool append = true); 26 | 27 | private: 28 | std::string path; 29 | std::string name; 30 | std::string playlistFile; 31 | 32 | double segmentDuration; 33 | }; 34 | 35 | }; // hls 36 | }; // transcoding 37 | 38 | #endif // NODE_TRANSCODING_HLS_PLAYLIST 39 | -------------------------------------------------------------------------------- /src/io/filereader.cpp: -------------------------------------------------------------------------------- 1 | #include "filereader.h" 2 | 3 | using namespace transcoding; 4 | using namespace transcoding::io; 5 | 6 | FileReader::FileReader(Handle source) : 7 | IOReader(source) { 8 | HandleScope scope; 9 | TC_LOG_D("FileReader::FileReader(%s)\n", *String::Utf8Value(source)); 10 | 11 | this->path = *String::AsciiValue(source); 12 | } 13 | 14 | FileReader::~FileReader() { 15 | TC_LOG_D("FileReader::~FileReader()\n"); 16 | } 17 | 18 | int FileReader::Open() { 19 | AVIOContext* s = NULL; 20 | int ret = avio_open(&s, this->path.c_str(), AVIO_RDONLY); 21 | if (ret) { 22 | TC_LOG_D("FileReader::Open(): failed (%d)\n", ret); 23 | return ret; 24 | } 25 | TC_LOG_D("FileReader::Open()\n"); 26 | this->context = s; 27 | return 0; 28 | } 29 | 30 | void FileReader::Close() { 31 | TC_LOG_D("FileReader::Close()\n"); 32 | avio_close(this->context); 33 | this->context = NULL; 34 | } 35 | -------------------------------------------------------------------------------- /src/io/filereader.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "../utils.h" 4 | #include "iohandle.h" 5 | 6 | #ifndef NODE_TRANSCODING_IO_FILEREADER 7 | #define NODE_TRANSCODING_IO_FILEREADER 8 | 9 | using namespace v8; 10 | 11 | namespace transcoding { 12 | namespace io { 13 | 14 | class FileReader : public IOReader { 15 | public: 16 | FileReader(Handle source); 17 | virtual ~FileReader(); 18 | 19 | virtual int Open(); 20 | virtual void Close(); 21 | 22 | public: 23 | std::string path; 24 | }; 25 | 26 | }; // io 27 | }; // transcoding 28 | 29 | #endif // NODE_TRANSCODING_IO_FILEREADER 30 | -------------------------------------------------------------------------------- /src/io/filewriter.cpp: -------------------------------------------------------------------------------- 1 | #include "filewriter.h" 2 | 3 | using namespace transcoding; 4 | using namespace transcoding::io; 5 | 6 | FileWriter::FileWriter(Handle source) : 7 | IOWriter(source) { 8 | HandleScope scope; 9 | TC_LOG_D("FileWriter::FileWriter(%s)\n", *String::Utf8Value(source)); 10 | 11 | this->path = *String::Utf8Value(source); 12 | } 13 | 14 | FileWriter::~FileWriter() { 15 | TC_LOG_D("FileWriter::~FileWriter()\n"); 16 | } 17 | 18 | int FileWriter::Open() { 19 | AVIOContext* s = NULL; 20 | int ret = avio_open(&s, this->path.c_str(), AVIO_WRONLY); 21 | if (ret) { 22 | TC_LOG_D("FileWriter::Open(): failed (%d)\n", ret); 23 | return ret; 24 | } 25 | TC_LOG_D("FileWriter::Open()\n"); 26 | this->context = s; 27 | return 0; 28 | } 29 | 30 | void FileWriter::Close() { 31 | TC_LOG_D("FileWriter::Close()\n"); 32 | avio_close(this->context); 33 | this->context = NULL; 34 | } 35 | -------------------------------------------------------------------------------- /src/io/filewriter.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "../utils.h" 4 | #include "iohandle.h" 5 | 6 | #ifndef NODE_TRANSCODING_IO_FILEWRITER 7 | #define NODE_TRANSCODING_IO_FILEWRITER 8 | 9 | using namespace v8; 10 | 11 | namespace transcoding { 12 | namespace io { 13 | 14 | class FileWriter : public IOWriter { 15 | public: 16 | FileWriter(Handle source); 17 | virtual ~FileWriter(); 18 | 19 | virtual int Open(); 20 | virtual void Close(); 21 | 22 | public: 23 | std::string path; 24 | }; 25 | 26 | }; // io 27 | }; // transcoding 28 | 29 | #endif // NODE_TRANSCODING_IO_FILEWRITER 30 | -------------------------------------------------------------------------------- /src/io/io.cpp: -------------------------------------------------------------------------------- 1 | #include "io.h" 2 | 3 | using namespace transcoding; 4 | using namespace transcoding::io; 5 | 6 | AVFormatContext* transcoding::io::createInputContext( 7 | IOReader* input, int* pret) { 8 | TC_LOG_D("io::createInputContext()\n"); 9 | AVFormatContext* ctx = NULL; 10 | int ret = 0; 11 | *pret = 0; 12 | 13 | ctx = avformat_alloc_context(); 14 | if (!ctx) { 15 | ret = AVERROR_NOMEM; 16 | TC_LOG_D("io::createInputContext(): failed alloc ctx (%d)\n", ret); 17 | goto CLEANUP; 18 | } 19 | 20 | ret = input->Open(); 21 | if (ret) { 22 | TC_LOG_D("io::createInputContext(): failed open (%d)\n", ret); 23 | goto CLEANUP; 24 | } 25 | ctx->pb = input->context; 26 | if (!ctx->pb) { 27 | ret = AVERROR_NOENT; 28 | TC_LOG_D("io::createInputContext(): no pb (%d)\n", ret); 29 | goto CLEANUP; 30 | } 31 | 32 | ret = avformat_open_input(&ctx, "", NULL, NULL); 33 | if (ret < 0) { 34 | TC_LOG_D("io::createInputContext(): failed open_input (%d)\n", ret); 35 | goto CLEANUP; 36 | } 37 | 38 | // Prevent avio_close (which would hose us) 39 | ctx->flags |= AVFMT_FLAG_CUSTOM_IO; 40 | 41 | ret = av_find_stream_info(ctx); 42 | if (ret < 0) { 43 | TC_LOG_D("io::createInputContext(): failed find_stream_info (%d)\n", ret); 44 | goto CLEANUP; 45 | } 46 | 47 | TC_LOG_D("io::createInputContext(): success\n"); 48 | return ctx; 49 | 50 | CLEANUP: 51 | if (ctx) { 52 | avformat_free_context(ctx); 53 | } 54 | *pret = ret; 55 | return NULL; 56 | } 57 | 58 | AVFormatContext* transcoding::io::createOutputContext( 59 | IOWriter* output, int* pret) { 60 | TC_LOG_D("io::createOutputContext()\n"); 61 | AVFormatContext* ctx = NULL; 62 | int ret = 0; 63 | *pret = 0; 64 | 65 | ctx = avformat_alloc_context(); 66 | if (!ctx) { 67 | ret = AVERROR_NOMEM; 68 | TC_LOG_D("io::createOutputContext(): failed alloc ctx (%d)\n", ret); 69 | goto CLEANUP; 70 | } 71 | 72 | ret = output->Open(); 73 | if (ret) { 74 | TC_LOG_D("io::createOutputContext(): failed open (%d)\n", ret); 75 | goto CLEANUP; 76 | } 77 | ctx->pb = output->context; 78 | if (!ctx->pb) { 79 | ret = AVERROR_NOENT; 80 | TC_LOG_D("io::createOutputContext(): no pb (%d)\n", ret); 81 | goto CLEANUP; 82 | } 83 | 84 | TC_LOG_D("io::createOutputContext(): success\n"); 85 | return ctx; 86 | 87 | CLEANUP: 88 | if (ctx) { 89 | avformat_free_context(ctx); 90 | } 91 | *pret = ret; 92 | return NULL; 93 | } 94 | -------------------------------------------------------------------------------- /src/io/io.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "../utils.h" 4 | #include "iohandle.h" 5 | 6 | #ifndef NODE_TRANSCODING_IO 7 | #define NODE_TRANSCODING_IO 8 | 9 | using namespace v8; 10 | 11 | namespace transcoding { 12 | namespace io { 13 | 14 | AVFormatContext* createInputContext(IOReader* input, int* pret); 15 | AVFormatContext* createOutputContext(IOWriter* output, int* pret); 16 | 17 | }; // io 18 | }; // transcoding 19 | 20 | #endif // NODE_TRANSCODING_IO 21 | -------------------------------------------------------------------------------- /src/io/iohandle.cpp: -------------------------------------------------------------------------------- 1 | #include "iohandle.h" 2 | #include "filereader.h" 3 | #include "filewriter.h" 4 | #include "nullwriter.h" 5 | #include "streamreader.h" 6 | #include "streamwriter.h" 7 | 8 | using namespace transcoding; 9 | using namespace transcoding::io; 10 | 11 | IOHandle::IOHandle(Handle source) : 12 | context(NULL) { 13 | HandleScope scope; 14 | 15 | this->source = Persistent::New(source); 16 | } 17 | 18 | IOHandle::~IOHandle() { 19 | this->source.Dispose(); 20 | } 21 | 22 | void IOHandle::CloseWhenDone(IOHandle* handle) { 23 | if (handle->QueueCloseOnIdle()) { 24 | // Queued! Will be deleted at some point 25 | } else { 26 | // Not queued - delete now 27 | delete handle; 28 | } 29 | } 30 | 31 | bool IOHandle::QueueCloseOnIdle() { 32 | this->Close(); 33 | return false; 34 | } 35 | 36 | IOReader::IOReader(Handle source) : 37 | IOHandle(source) { 38 | } 39 | 40 | IOReader::~IOReader() { 41 | } 42 | 43 | IOReader* IOReader::Create(Handle source, size_t maxBufferedBytes) { 44 | HandleScope scope; 45 | 46 | if (source->IsStringObject()) { 47 | return new FileReader(source); 48 | } else { 49 | return new StreamReader(source, 50 | maxBufferedBytes ? maxBufferedBytes : STREAMREADER_MAX_SIZE); 51 | } 52 | } 53 | 54 | IOWriter::IOWriter(Handle source) : 55 | IOHandle(source) { 56 | } 57 | 58 | IOWriter::~IOWriter() { 59 | } 60 | 61 | IOWriter* IOWriter::Create(Handle source, size_t maxBufferedBytes) { 62 | HandleScope scope; 63 | 64 | if (source.IsEmpty() || source->IsNull()) { 65 | return new NullWriter(); 66 | } else if (source->IsStringObject()) { 67 | return new FileWriter(source); 68 | } else { 69 | return new StreamWriter(source, 70 | maxBufferedBytes ? maxBufferedBytes : STREAMWRITER_MAX_SIZE); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/io/iohandle.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "../utils.h" 4 | 5 | #ifndef NODE_TRANSCODING_IO_IOHANDLE 6 | #define NODE_TRANSCODING_IO_IOHANDLE 7 | 8 | using namespace v8; 9 | 10 | namespace transcoding { 11 | namespace io { 12 | 13 | class IOHandle { 14 | public: 15 | IOHandle(Handle source); 16 | virtual ~IOHandle(); 17 | 18 | virtual int Open() = 0; 19 | virtual void Close() = 0; 20 | 21 | static void CloseWhenDone(IOHandle* handle); 22 | virtual bool QueueCloseOnIdle(); 23 | 24 | public: 25 | Persistent source; 26 | AVIOContext* context; 27 | }; 28 | 29 | class IOReader : public IOHandle { 30 | public: 31 | IOReader(Handle source); 32 | virtual ~IOReader(); 33 | 34 | static IOReader* Create(Handle source, size_t maxBufferedBytes = 0); 35 | }; 36 | 37 | class IOWriter : public IOHandle { 38 | public: 39 | IOWriter(Handle source); 40 | virtual ~IOWriter(); 41 | 42 | static IOWriter* Create(Handle source, size_t maxBufferedBytes = 0); 43 | }; 44 | 45 | }; // io 46 | }; // transcoding 47 | 48 | #endif // NODE_TRANSCODING_IO_IOHANDLE 49 | -------------------------------------------------------------------------------- /src/io/nullwriter.cpp: -------------------------------------------------------------------------------- 1 | #include "nullwriter.h" 2 | 3 | using namespace transcoding; 4 | using namespace transcoding::io; 5 | 6 | NullWriter::NullWriter() : 7 | IOWriter(Object::New()) { 8 | HandleScope scope; 9 | TC_LOG_D("NullWriter::NullWriter()\n"); 10 | } 11 | 12 | NullWriter::~NullWriter() { 13 | TC_LOG_D("NullWriter::~NullWriter()\n"); 14 | } 15 | 16 | int NullWriter::Open() { 17 | AVIOContext* s = NULL; 18 | int ret = avio_open_dyn_buf(&s); 19 | if (ret) { 20 | TC_LOG_D("NullWriter::Open(): failed (%d)\n", ret); 21 | return ret; 22 | } 23 | TC_LOG_D("NullWriter::Open()\n"); 24 | this->context = s; 25 | return 0; 26 | } 27 | 28 | void NullWriter::Close() { 29 | TC_LOG_D("NullWriter::Close()\n"); 30 | uint8_t* buffer = NULL; 31 | int size = avio_close_dyn_buf(this->context, &buffer); 32 | av_free(buffer); 33 | this->context = NULL; 34 | } 35 | -------------------------------------------------------------------------------- /src/io/nullwriter.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "../utils.h" 4 | #include "iohandle.h" 5 | 6 | #ifndef NODE_TRANSCODING_IO_NULLWRITER 7 | #define NODE_TRANSCODING_IO_NULLWRITER 8 | 9 | using namespace v8; 10 | 11 | namespace transcoding { 12 | namespace io { 13 | 14 | class NullWriter : public IOWriter { 15 | public: 16 | NullWriter(); 17 | virtual ~NullWriter(); 18 | 19 | virtual int Open(); 20 | virtual void Close(); 21 | 22 | public: 23 | }; 24 | 25 | }; // io 26 | }; // transcoding 27 | 28 | #endif // NODE_TRANSCODING_IO_NULLWRITER 29 | -------------------------------------------------------------------------------- /src/io/streamreader.cpp: -------------------------------------------------------------------------------- 1 | #include "streamreader.h" 2 | #include 3 | 4 | using namespace node; 5 | using namespace transcoding; 6 | using namespace transcoding::io; 7 | 8 | // The stream reader works by listening for data events on the v8 thread 9 | // and queuing them up. The actual ReadPacket calls come across on a worker 10 | // thread and pop them off. If there are no buffers available, the worker thread 11 | // will block until there are. If there are too many buffers queued, the stream 12 | // is paused until buffers are drained. 13 | 14 | StreamReader::StreamReader(Handle source, size_t maxBufferedBytes) : 15 | IOReader(source), 16 | paused(false), err(0), eof(false), 17 | maxBufferedBytes(maxBufferedBytes), totalBufferredBytes(0) { 18 | TC_LOG_D("StreamReader::StreamReader()\n"); 19 | HandleScope scope; 20 | 21 | pthread_mutex_init(&this->lock, NULL); 22 | pthread_cond_init(&this->cond, NULL); 23 | 24 | this->asyncReq = new uv_async_t(); 25 | this->asyncReq->data = this; 26 | uv_async_init(uv_default_loop(), this->asyncReq, ResumeAsync); 27 | 28 | // TODO: support seeking? 29 | this->canSeek = false; 30 | 31 | // Pull out methods we will use frequently 32 | this->sourcePause = Persistent::New( 33 | this->source->Get(String::New("pause")).As()); 34 | this->sourceResume = Persistent::New( 35 | this->source->Get(String::New("resume")).As()); 36 | 37 | // Add events to stream 38 | NODE_ON_EVENT(source, "data", onData, OnData, this); 39 | NODE_ON_EVENT(source, "end", onEnd, OnEnd, this); 40 | NODE_ON_EVENT(source, "close", onClose, OnClose, this); 41 | NODE_ON_EVENT(source, "error", onError, OnError, this); 42 | 43 | // Kick off the stream (some sources need this) 44 | if (!this->sourceResume.IsEmpty()) { 45 | this->sourceResume->Call(this->source, 0, NULL); 46 | } 47 | } 48 | 49 | StreamReader::~StreamReader() { 50 | TC_LOG_D("StreamReader::~StreamReader()\n"); 51 | HandleScope scope; 52 | 53 | pthread_cond_destroy(&this->cond); 54 | pthread_mutex_destroy(&this->lock); 55 | 56 | this->sourcePause.Dispose(); 57 | this->sourceResume.Dispose(); 58 | 59 | uv_close((uv_handle_t*)this->asyncReq, AsyncHandleClose); 60 | } 61 | 62 | int StreamReader::Open() { 63 | TC_LOG_D("StreamReader::Open()\n"); 64 | int bufferSize = STREAMREADER_BUFFER_SIZE; 65 | uint8_t* buffer = (uint8_t*)av_malloc(bufferSize); 66 | AVIOContext* s = avio_alloc_context( 67 | buffer, bufferSize, 68 | 0, // 1 = write 69 | this, 70 | ReadPacket, NULL, this->canSeek ? Seek : NULL); 71 | s->seekable = 0; // AVIO_SEEKABLE_NORMAL 72 | this->context = s; 73 | return 0; 74 | } 75 | 76 | void StreamReader::Close() { 77 | TC_LOG_D("StreamReader::Close()\n"); 78 | HandleScope scope; 79 | Handle source = this->source; 80 | 81 | // Unbind all events 82 | NODE_REMOVE_EVENT(source, "data", onData); 83 | NODE_REMOVE_EVENT(source, "end", onEnd); 84 | NODE_REMOVE_EVENT(source, "close", onClose); 85 | NODE_REMOVE_EVENT(source, "error", onError); 86 | 87 | bool readable = source->Get(String::New("readable"))->IsTrue(); 88 | if (readable) { 89 | Local destroySoon = 90 | Local::Cast(source->Get(String::New("destroySoon"))); 91 | if (!destroySoon.IsEmpty()) { 92 | destroySoon->Call(source, 0, NULL); 93 | } 94 | } 95 | 96 | if (this->context->buffer) { 97 | av_free(this->context->buffer); 98 | } 99 | av_free(this->context); 100 | this->context = NULL; 101 | } 102 | 103 | Handle StreamReader::OnData(const Arguments& args) { 104 | HandleScope scope; 105 | StreamReader* stream = 106 | static_cast(External::Unwrap(args.Data())); 107 | 108 | Local buffer = Local::Cast(args[0]); 109 | ReadBuffer* readBuffer = new ReadBuffer( 110 | (uint8_t*)Buffer::Data(buffer), Buffer::Length(buffer)); 111 | 112 | TC_LOG_D("StreamReader::OnData(): %d new bytes\n", 113 | (int)Buffer::Length(buffer)); 114 | 115 | pthread_mutex_lock(&stream->lock); 116 | 117 | stream->buffers.push_back(readBuffer); 118 | stream->totalBufferredBytes += readBuffer->length; 119 | 120 | // Check for max buffer condition 121 | bool needsPause = false; 122 | if (stream->totalBufferredBytes > stream->maxBufferedBytes) { 123 | if (!stream->sourcePause.IsEmpty()) { 124 | needsPause = true; 125 | stream->paused = true; 126 | } 127 | } 128 | 129 | //printf("OnData: buffer %lld/%lld, paused: %d\n", 130 | // stream->totalBufferredBytes, stream->maxBufferedBytes, stream->paused); 131 | 132 | pthread_cond_signal(&stream->cond); 133 | pthread_mutex_unlock(&stream->lock); 134 | 135 | if (needsPause) { 136 | TC_LOG_D("StreamReader::OnData(): buffer full, pausing\n"); 137 | stream->sourcePause->Call(stream->source, 0, NULL); 138 | } 139 | 140 | return scope.Close(Undefined()); 141 | } 142 | 143 | Handle StreamReader::OnEnd(const Arguments& args) { 144 | HandleScope scope; 145 | StreamReader* stream = 146 | static_cast(External::Unwrap(args.Data())); 147 | 148 | TC_LOG_D("StreamReader::OnEnd()\n"); 149 | 150 | pthread_mutex_lock(&stream->lock); 151 | stream->eof = true; 152 | pthread_cond_signal(&stream->cond); 153 | pthread_mutex_unlock(&stream->lock); 154 | 155 | return scope.Close(Undefined()); 156 | } 157 | 158 | Handle StreamReader::OnClose(const Arguments& args) { 159 | HandleScope scope; 160 | StreamReader* stream = 161 | static_cast(External::Unwrap(args.Data())); 162 | 163 | TC_LOG_D("StreamReader::OnClose()\n"); 164 | 165 | pthread_mutex_lock(&stream->lock); 166 | stream->eof = true; 167 | pthread_cond_signal(&stream->cond); 168 | pthread_mutex_unlock(&stream->lock); 169 | 170 | return scope.Close(Undefined()); 171 | } 172 | 173 | Handle StreamReader::OnError(const Arguments& args) { 174 | HandleScope scope; 175 | StreamReader* stream = 176 | static_cast(External::Unwrap(args.Data())); 177 | 178 | TC_LOG_D("StreamReader::OnError(): %s\n", 179 | *String::Utf8Value(args[0]->ToString())); 180 | 181 | pthread_mutex_lock(&stream->lock); 182 | stream->err = AVERROR_IO; 183 | pthread_cond_signal(&stream->cond); 184 | pthread_mutex_unlock(&stream->lock); 185 | 186 | return scope.Close(Undefined()); 187 | } 188 | 189 | void StreamReader::ResumeAsync(uv_async_t* handle, int status) { 190 | HandleScope scope; 191 | assert(status == 0); 192 | StreamReader* stream = static_cast(handle->data); 193 | 194 | if (!stream->sourceResume.IsEmpty()) { 195 | TC_LOG_D("StreamReader::ResumeAsync()\n"); 196 | stream->sourceResume->Call(stream->source, 0, NULL); 197 | } 198 | } 199 | 200 | void StreamReader::AsyncHandleClose(uv_handle_t* handle) { 201 | TC_LOG_D("StreamReader::AsyncHandleClose()\n"); 202 | delete handle; 203 | } 204 | 205 | int StreamReader::ReadPacket(void* opaque, uint8_t* buffer, int bufferSize) { 206 | StreamReader* stream = static_cast(opaque); 207 | 208 | int ret = 0; 209 | bool needsResume = false; 210 | 211 | pthread_mutex_lock(&stream->lock); 212 | 213 | // Wait until some bytes are available 214 | while (!stream->err && !stream->eof && !stream->totalBufferredBytes) { 215 | pthread_cond_wait(&stream->cond, &stream->lock); 216 | } 217 | 218 | if (stream->err) { 219 | // Stream error 220 | TC_LOG_D("StreamReader::ReadPacket(): stream error (%d)\n", stream->err); 221 | ret = stream->err; 222 | } else if (stream->totalBufferredBytes) { 223 | // Read the next buffer 224 | ReadBuffer* nextBuffer = stream->buffers.front(); 225 | size_t bytesRead = nextBuffer->Read(buffer, bufferSize); 226 | stream->totalBufferredBytes -= bytesRead; 227 | assert(stream->totalBufferredBytes >= 0); 228 | if (nextBuffer->IsEmpty()) { 229 | stream->buffers.erase(stream->buffers.begin()); 230 | delete nextBuffer; 231 | } 232 | TC_LOG_D("StreamReader::ReadPacket(): reading %d bytes\n", (int)bytesRead); 233 | ret = (int)bytesRead; 234 | } else if (stream->eof) { 235 | // Stream at EOF 236 | TC_LOG_D("StreamReader::ReadPacket(): stream eof\n"); 237 | ret = 0; // eof 238 | } else { 239 | // Stream in error (or unknown, so return EOF) 240 | TC_LOG_D("StreamReader::ReadPacket(): stream UNKNOWN (%d)\n", stream->err); 241 | ret = stream->err; 242 | } 243 | 244 | if (stream->paused && ret > 0) { 245 | // Stream is paused - restart it 246 | if (stream->totalBufferredBytes < stream->maxBufferedBytes) { 247 | needsResume = true; 248 | stream->paused = false; 249 | } 250 | } 251 | 252 | pthread_mutex_unlock(&stream->lock); 253 | 254 | //printf("ReadPacket: buffer %lld/%lld, paused: %d, resuming: %d\n", 255 | // stream->totalBufferredBytes, stream->maxBufferedBytes, stream->paused, 256 | // needsResume); 257 | 258 | if (needsResume) { 259 | TC_LOG_D("StreamReader::ReadPacket(): resuming stream\n"); 260 | uv_async_send(stream->asyncReq); 261 | } 262 | 263 | return ret; 264 | } 265 | 266 | int64_t StreamReader::Seek(void* opaque, int64_t offset, int whence) { 267 | StreamReader* stream = static_cast(opaque); 268 | // TODO: seek 269 | return 0; 270 | } 271 | 272 | ReadBuffer::ReadBuffer(uint8_t* source, int64_t length) : 273 | offset(0), data(NULL), length(length) { 274 | this->length = length; 275 | this->data = new uint8_t[this->length]; 276 | memcpy(this->data, source, this->length); 277 | } 278 | 279 | ReadBuffer::~ReadBuffer() { 280 | delete[] this->data; 281 | this->data = NULL; 282 | } 283 | 284 | bool ReadBuffer::IsEmpty() { 285 | return this->offset >= this->length || !this->length; 286 | } 287 | 288 | int64_t ReadBuffer::Read(uint8_t* buffer, int64_t bufferSize) { 289 | int64_t toRead = std::min(bufferSize, this->length - this->offset); 290 | if (toRead) { 291 | memcpy(buffer, this->data + this->offset, toRead); 292 | this->offset += toRead; 293 | } 294 | return toRead; 295 | } 296 | -------------------------------------------------------------------------------- /src/io/streamreader.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "../utils.h" 6 | #include "iohandle.h" 7 | 8 | #ifndef NODE_TRANSCODING_IO_STREAMREADER 9 | #define NODE_TRANSCODING_IO_STREAMREADER 10 | 11 | using namespace v8; 12 | 13 | namespace transcoding { 14 | namespace io { 15 | 16 | #define STREAMREADER_BUFFER_SIZE (64 * 1024) 17 | #define STREAMREADER_MAX_SIZE (32 * 1024 * 1024) 18 | 19 | class ReadBuffer { 20 | public: 21 | ReadBuffer(uint8_t* source, int64_t length); 22 | ~ReadBuffer(); 23 | 24 | bool IsEmpty(); 25 | int64_t Read(uint8_t* buffer, int64_t bufferSize); 26 | 27 | public: 28 | int64_t offset; 29 | uint8_t* data; 30 | int64_t length; 31 | }; 32 | 33 | class StreamReader : public IOReader { 34 | public: 35 | StreamReader(Handle source, 36 | size_t maxBufferedBytes = STREAMREADER_MAX_SIZE); 37 | virtual ~StreamReader(); 38 | 39 | virtual int Open(); 40 | virtual void Close(); 41 | 42 | private: 43 | static Handle OnData(const Arguments& args); 44 | static Handle OnEnd(const Arguments& args); 45 | static Handle OnClose(const Arguments& args); 46 | static Handle OnError(const Arguments& args); 47 | 48 | static void ResumeAsync(uv_async_t* handle, int status); 49 | static void AsyncHandleClose(uv_handle_t* handle); 50 | 51 | static int ReadPacket(void* opaque, uint8_t* buffer, int bufferSize); 52 | static int64_t Seek(void* opaque, int64_t offset, int whence); 53 | 54 | public: 55 | bool canSeek; 56 | 57 | Persistent sourcePause; 58 | Persistent sourceResume; 59 | 60 | Persistent onData; 61 | Persistent onEnd; 62 | Persistent onClose; 63 | Persistent onError; 64 | 65 | uv_async_t* asyncReq; 66 | 67 | pthread_mutex_t lock; 68 | pthread_cond_t cond; 69 | bool paused; 70 | int err; 71 | bool eof; 72 | int64_t maxBufferedBytes; 73 | int64_t totalBufferredBytes; 74 | std::vector buffers; 75 | }; 76 | 77 | }; // io 78 | }; // transcoding 79 | 80 | #endif // NODE_TRANSCODING_IO_STREAMREADER 81 | -------------------------------------------------------------------------------- /src/io/streamwriter.cpp: -------------------------------------------------------------------------------- 1 | #include "streamwriter.h" 2 | #include 3 | 4 | using namespace node; 5 | using namespace transcoding; 6 | using namespace transcoding::io; 7 | 8 | // The stream writer works by firing async write requests to the v8 thread to 9 | // pass off to the stream object. When the stream object indicates that its 10 | // buffers are full the WritePacket calls occuring on the background thread will 11 | // block. Once the stream object has drained, signalling it's ok to write again, 12 | // writes will continue. 13 | 14 | StreamWriter::StreamWriter(Handle source, size_t maxBufferedBytes) : 15 | IOWriter(source), 16 | idle(NULL), 17 | kernelBufferFull(false), selfBufferFull(false), 18 | err(0), eof(false), closing(false), 19 | maxBufferedBytes(maxBufferedBytes), totalBufferredBytes(0) { 20 | TC_LOG_D("StreamWriter::StreamWriter()\n"); 21 | HandleScope scope; 22 | 23 | pthread_mutex_init(&this->lock, NULL); 24 | pthread_cond_init(&this->cond, NULL); 25 | 26 | // TODO: support seeking? 27 | this->canSeek = false; 28 | 29 | // Add events to stream 30 | // TODO: keep self alive somehow? 31 | NODE_ON_EVENT(source, "drain", onDrain, OnDrain, this); 32 | NODE_ON_EVENT(source, "close", onClose, OnClose, this); 33 | NODE_ON_EVENT(source, "error", onError, OnError, this); 34 | } 35 | 36 | StreamWriter::~StreamWriter() { 37 | TC_LOG_D("StreamWriter::~StreamWriter()\n"); 38 | 39 | pthread_mutex_lock(&this->lock); 40 | if (this->buffers.size()) { 41 | TC_LOG_D("StreamWriter::~StreamWriter(): dtor with %d writes pending\n", 42 | (int)this->buffers.size()); 43 | } 44 | pthread_mutex_unlock(&this->lock); 45 | pthread_cond_destroy(&this->cond); 46 | pthread_mutex_destroy(&this->lock); 47 | 48 | // NOTE: do not delete this->idle, it is cleaned up by the handle close stuff 49 | } 50 | 51 | int StreamWriter::Open() { 52 | TC_LOG_D("StreamWriter::Open()\n"); 53 | 54 | int bufferSize = STREAMWRITER_BUFFER_SIZE; 55 | uint8_t* buffer = (uint8_t*)av_malloc(bufferSize); 56 | AVIOContext* s = avio_alloc_context( 57 | buffer, bufferSize, 58 | 1, // 1 = write 59 | this, 60 | NULL, WritePacket, this->canSeek ? Seek : NULL); 61 | s->seekable = 0; // AVIO_SEEKABLE_NORMAL 62 | this->context = s; 63 | 64 | this->idle = new uv_idle_t(); 65 | this->idle->data = this; 66 | uv_idle_init(uv_default_loop(), this->idle); 67 | uv_idle_start(this->idle, IdleCallback); 68 | 69 | return 0; 70 | } 71 | 72 | void StreamWriter::Close() { 73 | TC_LOG_D("StreamWriter::Close()\n"); 74 | HandleScope scope; 75 | Local source = Local::New(this->source); 76 | 77 | pthread_mutex_lock(&this->lock); 78 | if (this->buffers.size()) { 79 | TC_LOG_D("StreamWriter::Close(): close when %d writes pending\n", 80 | (int)this->buffers.size()); 81 | } 82 | pthread_mutex_unlock(&this->lock); 83 | 84 | // Unbind all events 85 | NODE_REMOVE_EVENT(source, "drain", onDrain); 86 | NODE_REMOVE_EVENT(source, "close", onClose); 87 | NODE_REMOVE_EVENT(source, "error", onError); 88 | 89 | bool writable = source->Get(String::New("writable"))->IsTrue(); 90 | if (writable) { 91 | Local end = 92 | Local::Cast(this->source->Get(String::New("end"))); 93 | end->Call(this->source, 0, NULL); 94 | 95 | Local destroySoon = 96 | Local::Cast(source->Get(String::New("destroySoon"))); 97 | if (!destroySoon.IsEmpty()) { 98 | destroySoon->Call(source, 0, NULL); 99 | } 100 | } 101 | 102 | if (this->context->buffer) { 103 | av_free(this->context->buffer); 104 | } 105 | av_free(this->context); 106 | this->context = NULL; 107 | 108 | // Kill the idle ASAP 109 | uv_close((uv_handle_t*)this->idle, IdleHandleClose); 110 | } 111 | 112 | bool StreamWriter::QueueCloseOnIdle() { 113 | TC_LOG_D("StreamWriter::QueueCloseOnIdle()\n"); 114 | 115 | pthread_mutex_lock(&this->lock); 116 | int pendingWrites = this->buffers.size(); 117 | this->closing = true; 118 | pthread_cond_signal(&this->cond); 119 | pthread_mutex_unlock(&this->lock); 120 | if (!pendingWrites) { 121 | // Close immediately 122 | TC_LOG_D("StreamWriter::QueueCloseOnIdle(): closing immediately\n"); 123 | this->Close(); 124 | } else { 125 | TC_LOG_D("StreamWriter::QueueCloseOnIdle(): deferred close, %d pending\n", 126 | pendingWrites); 127 | } 128 | return pendingWrites > 0; 129 | } 130 | 131 | Handle StreamWriter::OnDrain(const Arguments& args) { 132 | TC_LOG_D("StreamWriter::OnDrain()\n"); 133 | HandleScope scope; 134 | StreamWriter* stream = 135 | static_cast(External::Unwrap(args.Data())); 136 | 137 | pthread_mutex_lock(&stream->lock); 138 | stream->kernelBufferFull = false; 139 | pthread_cond_signal(&stream->cond); 140 | pthread_mutex_unlock(&stream->lock); 141 | 142 | return scope.Close(Undefined()); 143 | } 144 | 145 | Handle StreamWriter::OnClose(const Arguments& args) { 146 | TC_LOG_D("StreamWriter::OnClose()\n"); 147 | HandleScope scope; 148 | StreamWriter* stream = 149 | static_cast(External::Unwrap(args.Data())); 150 | 151 | pthread_mutex_lock(&stream->lock); 152 | stream->eof = true; 153 | pthread_cond_signal(&stream->cond); 154 | pthread_mutex_unlock(&stream->lock); 155 | 156 | return scope.Close(Undefined()); 157 | } 158 | 159 | Handle StreamWriter::OnError(const Arguments& args) { 160 | HandleScope scope; 161 | StreamWriter* stream = 162 | static_cast(External::Unwrap(args.Data())); 163 | 164 | TC_LOG_D("StreamWriter::OnError(): %s\n", 165 | *String::Utf8Value(args[0]->ToString())); 166 | 167 | pthread_mutex_lock(&stream->lock); 168 | stream->err = AVERROR_IO; 169 | pthread_cond_signal(&stream->cond); 170 | pthread_mutex_unlock(&stream->lock); 171 | 172 | return scope.Close(Undefined()); 173 | } 174 | 175 | void StreamWriterFreeCallback(char *data, void *hint) { 176 | uint8_t* ptr = (uint8_t*)data; 177 | delete[] ptr; 178 | } 179 | 180 | void StreamWriter::IdleCallback(uv_idle_t* handle, int status) { 181 | HandleScope scope; 182 | assert(status == 0); 183 | StreamWriter* stream = static_cast(handle->data); 184 | 185 | if (stream->kernelBufferFull) { 186 | // Don't do anything this tick 187 | return; 188 | } 189 | 190 | bool needsDestruction = false; 191 | 192 | while (true) { 193 | WriteBuffer* buffer = NULL; 194 | pthread_mutex_lock(&stream->lock); 195 | int remaining = stream->buffers.size(); 196 | if (remaining) { 197 | buffer = stream->buffers.front(); 198 | stream->buffers.erase(stream->buffers.begin()); 199 | } else { 200 | needsDestruction = stream->closing; 201 | } 202 | pthread_mutex_unlock(&stream->lock); 203 | if (!buffer) { 204 | break; 205 | } 206 | 207 | TC_LOG_D("StreamWriter::IdleCallback(): write, %d remaining, closing: %d\n", 208 | remaining, stream->closing); 209 | 210 | // TODO: coalesce buffers (if 100 buffers waiting, merge?) 211 | // Wrap our buffer in the node buffer (pass reference) 212 | int bufferLength = (int)buffer->length; 213 | Buffer* bufferObj = Buffer::New( 214 | (char*)buffer->data, bufferLength, StreamWriterFreeCallback, NULL); 215 | Local bufferLocal = Local::New(bufferObj->handle_); 216 | buffer->Steal(); 217 | delete buffer; 218 | 219 | Local write = 220 | Local::Cast(stream->source->Get(String::New("write"))); 221 | bool bufferDrained = 222 | write->Call(stream->source, 1, (Handle[]){ 223 | bufferLocal, 224 | })->IsTrue(); 225 | 226 | pthread_mutex_lock(&stream->lock); 227 | if (!stream->kernelBufferFull && !bufferDrained) { 228 | // Buffer now full - wait until drain 229 | stream->kernelBufferFull = true; 230 | TC_LOG_D("StreamWriter::IdleCallback(): kernel buffer full\n"); 231 | } 232 | stream->totalBufferredBytes -= bufferLength; 233 | 234 | // Drain until the buffer has a bit of room 235 | bool bufferFull = 236 | stream->totalBufferredBytes + STREAMWRITER_BUFFER_SIZE * 4 >= 237 | stream->maxBufferedBytes; 238 | if (stream->selfBufferFull && !bufferFull) { 239 | stream->selfBufferFull = false; 240 | TC_LOG_D("StreamWriter::IdleCallback(): self buffer drained\n"); 241 | } 242 | 243 | pthread_cond_signal(&stream->cond); 244 | pthread_mutex_unlock(&stream->lock); 245 | if (!bufferDrained) { 246 | // Don't consume any more buffers 247 | break; 248 | } 249 | 250 | // Can get in very bad tight loops with this logic - only write until there 251 | // is room enough in the buffer for more writes 252 | // Always drain when closing, though! 253 | // TODO: when closing, do a nice staggered write sequence through the main 254 | // event loop - if there are many pending writes this can block for awhile! 255 | if (bufferFull) { 256 | TC_LOG_D("StreamWriter::IdleCallback(): self buffer drained, break\n"); 257 | break; 258 | } 259 | } 260 | 261 | if (needsDestruction) { 262 | TC_LOG_D("StreamWriter::IdleCallback(): closing at end of writes\n"); 263 | stream->Close(); 264 | delete stream; 265 | } 266 | } 267 | 268 | void StreamWriter::IdleHandleClose(uv_handle_t* handle) { 269 | TC_LOG_D("StreamWriter::IdleHandleClose()\n"); 270 | delete handle; 271 | } 272 | 273 | int StreamWriter::WritePacket(void* opaque, uint8_t* buffer, int bufferSize) { 274 | TC_LOG_D("StreamWriter::WritePacket(%d)\n", bufferSize); 275 | StreamWriter* stream = static_cast(opaque); 276 | 277 | int ret = 0; 278 | bool needsResume = false; 279 | 280 | pthread_mutex_lock(&stream->lock); 281 | 282 | if (!stream->selfBufferFull && 283 | stream->totalBufferredBytes + bufferSize > stream->maxBufferedBytes) { 284 | TC_LOG_D("StreamWriter::WritePacket(): self buffer full\n"); 285 | stream->selfBufferFull = true; 286 | } 287 | 288 | // Wait until the target is drained 289 | while (!stream->err && !stream->eof && 290 | (stream->kernelBufferFull || stream->selfBufferFull)) { 291 | pthread_cond_wait(&stream->cond, &stream->lock); 292 | } 293 | 294 | TC_LOG_D("StreamWriter::WritePacket(): %lld/%lld, eof %d, err %d, kbf %d, sbf %d\n", 295 | stream->totalBufferredBytes, stream->maxBufferedBytes, 296 | stream->eof, stream->err, 297 | stream->kernelBufferFull, stream->selfBufferFull); 298 | 299 | if (stream->err) { 300 | // Stream error 301 | ret = stream->err; 302 | TC_LOG_D("StreamWriter::WritePacket(): stream error (%d)\n", stream->err); 303 | } else if (stream->totalBufferredBytes < stream->maxBufferedBytes) { 304 | // Write the next buffer 305 | WriteBuffer* nextBuffer = new WriteBuffer((uint8_t*)buffer, bufferSize); 306 | stream->buffers.push_back(nextBuffer); 307 | stream->totalBufferredBytes += nextBuffer->length; 308 | 309 | TC_LOG_D("StreamWriter::WritePacket(): write of %d bytes\n", bufferSize); 310 | 311 | ret = bufferSize; 312 | } else if (stream->eof) { 313 | // Stream at EOF 314 | TC_LOG_D("StreamWriter::WritePacket(): stream eof\n"); 315 | ret = 0; // eof 316 | } else { 317 | // Stream in error (or unknown, so return EOF) 318 | TC_LOG_D("StreamWriter::WritePacket(): stream UNKNOWN (%d)\n", stream->err); 319 | ret = stream->err; 320 | } 321 | 322 | pthread_mutex_unlock(&stream->lock); 323 | 324 | return ret; 325 | } 326 | 327 | int64_t StreamWriter::Seek(void* opaque, int64_t offset, int whence) { 328 | StreamWriter* stream = static_cast(opaque); 329 | // TODO: seek 330 | return 0; 331 | } 332 | 333 | WriteBuffer::WriteBuffer(uint8_t* source, int64_t length) : 334 | data(NULL), length(length) { 335 | this->length = length; 336 | this->data = new uint8_t[this->length]; 337 | memcpy(this->data, source, this->length); 338 | } 339 | 340 | WriteBuffer::~WriteBuffer() { 341 | if (this->data) { 342 | delete[] this->data; 343 | this->data = NULL; 344 | } 345 | } 346 | 347 | void WriteBuffer::Steal() { 348 | this->data = NULL; 349 | this->length = 0; 350 | } 351 | -------------------------------------------------------------------------------- /src/io/streamwriter.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "../utils.h" 6 | #include "iohandle.h" 7 | 8 | #ifndef NODE_TRANSCODING_IO_STREAMWRITER 9 | #define NODE_TRANSCODING_IO_STREAMWRITER 10 | 11 | using namespace v8; 12 | 13 | namespace transcoding { 14 | namespace io { 15 | 16 | #define STREAMWRITER_BUFFER_SIZE (64 * 1024) 17 | #define STREAMWRITER_MAX_SIZE (1 * 1024 * 1024) 18 | 19 | class WriteBuffer { 20 | public: 21 | WriteBuffer(uint8_t* source, int64_t length); 22 | ~WriteBuffer(); 23 | 24 | void Steal(); 25 | 26 | public: 27 | uint8_t* data; 28 | int64_t length; 29 | }; 30 | 31 | class StreamWriter : public IOWriter { 32 | public: 33 | StreamWriter(Handle source, 34 | size_t maxBufferedBytes = STREAMWRITER_MAX_SIZE); 35 | virtual ~StreamWriter(); 36 | 37 | virtual int Open(); 38 | virtual void Close(); 39 | 40 | virtual bool QueueCloseOnIdle(); 41 | 42 | private: 43 | static Handle OnDrain(const Arguments& args); 44 | static Handle OnClose(const Arguments& args); 45 | static Handle OnError(const Arguments& args); 46 | 47 | static void IdleCallback(uv_idle_t* handle, int status); 48 | static void IdleHandleClose(uv_handle_t* handle); 49 | 50 | static int WritePacket(void* opaque, uint8_t* buffer, int bufferSize); 51 | static int64_t Seek(void* opaque, int64_t offset, int whence); 52 | 53 | public: 54 | bool canSeek; 55 | 56 | Persistent onDrain; 57 | Persistent onClose; 58 | Persistent onError; 59 | 60 | uv_idle_t* idle; 61 | 62 | pthread_mutex_t lock; 63 | pthread_cond_t cond; 64 | bool kernelBufferFull; 65 | bool selfBufferFull; 66 | int err; 67 | bool eof; 68 | bool closing; 69 | int64_t maxBufferedBytes; 70 | int64_t totalBufferredBytes; 71 | std::vector buffers; 72 | }; 73 | 74 | }; // io 75 | }; // transcoding 76 | 77 | #endif // NODE_TRANSCODING_IO_STREAMWRITER 78 | -------------------------------------------------------------------------------- /src/mediainfo.cpp: -------------------------------------------------------------------------------- 1 | #include "mediainfo.h" 2 | 3 | using namespace transcoding; 4 | using namespace v8; 5 | 6 | Handle transcoding::createMediaInfo(AVFormatContext* ctx, bool encoding) { 7 | HandleScope scope; 8 | 9 | // Lop off just the first container name 10 | // e.g., mov,mp4,m4a.... -> mov 11 | char container[256]; 12 | strcpy(container, encoding ? ctx->oformat->name : ctx->iformat->name); 13 | char* containerHead = strchr(container, ','); 14 | if (containerHead) { 15 | *containerHead = 0; 16 | } 17 | 18 | Local result = Object::New(); 19 | 20 | result->Set(String::New("container"), String::New(container)); 21 | if (ctx->duration != AV_NOPTS_VALUE) { 22 | result->Set(String::New("duration"), 23 | Number::New(ctx->duration / (double)AV_TIME_BASE)); 24 | } 25 | if (ctx->start_time != AV_NOPTS_VALUE) { 26 | result->Set(String::New("start"), 27 | Number::New(ctx->start_time / (double)AV_TIME_BASE)); 28 | } 29 | if (ctx->bit_rate) { 30 | result->Set(String::New("bitrate"), 31 | Number::New(ctx->bit_rate)); 32 | } 33 | 34 | Local streams = Array::New(); 35 | result->Set(String::New("streams"), streams); 36 | for (int n = 0; n < ctx->nb_streams; n++) { 37 | AVStream* st = ctx->streams[n]; 38 | AVCodecContext* sc = st->codec; 39 | 40 | Local stream = Object::New(); 41 | streams->Set(streams->Length(), stream); 42 | 43 | switch (sc->codec_type) { 44 | case AVMEDIA_TYPE_VIDEO: do { 45 | stream->Set(String::New("type"), String::New("video")); 46 | Local resolution = Object::New(); 47 | resolution->Set(String::New("width"), Number::New(sc->width)); 48 | resolution->Set(String::New("height"), Number::New(sc->height)); 49 | stream->Set(String::New("resolution"), resolution); 50 | if (st->avg_frame_rate.den && st->avg_frame_rate.num) { 51 | stream->Set(String::New("fps"), 52 | Number::New(av_q2d(st->avg_frame_rate))); 53 | } else if (st->r_frame_rate.den && st->r_frame_rate.num) { 54 | stream->Set(String::New("fps"), 55 | Number::New(av_q2d(st->r_frame_rate))); 56 | } 57 | } while(0); break; 58 | case AVMEDIA_TYPE_AUDIO: do { 59 | stream->Set(String::New("type"), String::New("audio")); 60 | stream->Set(String::New("channels"), Number::New(sc->channels)); 61 | stream->Set(String::New("sampleRate"), Number::New(sc->sample_rate)); 62 | const char* sampleFormat = "unknown"; 63 | switch (sc->sample_fmt) { 64 | default: 65 | case AV_SAMPLE_FMT_NONE: 66 | break; 67 | case AV_SAMPLE_FMT_U8: 68 | sampleFormat = "u8"; 69 | break; 70 | case AV_SAMPLE_FMT_S16: 71 | sampleFormat = "s16"; 72 | break; 73 | case AV_SAMPLE_FMT_S32: 74 | sampleFormat = "s32"; 75 | break; 76 | case AV_SAMPLE_FMT_FLT: 77 | sampleFormat = "flt"; 78 | break; 79 | case AV_SAMPLE_FMT_DBL: 80 | sampleFormat = "dbl"; 81 | break; 82 | } 83 | stream->Set(String::New("sampleFormat"), String::New(sampleFormat)); 84 | } while(0); break; 85 | case AVMEDIA_TYPE_SUBTITLE: 86 | stream->Set(String::New("type"), String::New("subtitle")); 87 | break; 88 | default: 89 | // Other - ignore 90 | continue; 91 | } 92 | 93 | AVCodec* codec = false ? 94 | avcodec_find_encoder(sc->codec_id) : 95 | avcodec_find_decoder(sc->codec_id); 96 | if (codec) { 97 | stream->Set(String::New("codec"), String::New(codec->name)); 98 | if (sc->profile != FF_PROFILE_UNKNOWN) { 99 | const char* codecProfile = av_get_profile_name(codec, sc->profile); 100 | if (codecProfile) { 101 | stream->Set(String::New("profile"), String::New(codecProfile)); 102 | } 103 | stream->Set(String::New("profileId"), Number::New(sc->profile)); 104 | stream->Set(String::New("profileLevel"), Number::New(sc->level)); 105 | } 106 | } 107 | 108 | if (sc->bit_rate) { 109 | stream->Set(String::New("bitrate"), Number::New(sc->bit_rate)); 110 | } 111 | 112 | AVDictionaryEntry* lang = av_dict_get(st->metadata, "language", NULL, 0); 113 | if (lang) { 114 | // Not in ISO format - often 'eng' or something 115 | // TODO: convert language to something usable 116 | if (strcmp(lang->value, "und") != 0) { 117 | stream->Set(String::New("language"), String::New(lang->value)); 118 | } 119 | } 120 | } 121 | 122 | return scope.Close(result); 123 | } 124 | -------------------------------------------------------------------------------- /src/mediainfo.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "utils.h" 4 | 5 | #ifndef NODE_TRANSCODING_MEDIAINFO 6 | #define NODE_TRANSCODING_MEDIAINFO 7 | 8 | using namespace v8; 9 | 10 | namespace transcoding { 11 | 12 | Handle createMediaInfo(AVFormatContext* ctx, bool encoding); 13 | 14 | }; // transcoding 15 | 16 | #endif // NODE_TRANSCODING_MEDIAINFO 17 | -------------------------------------------------------------------------------- /src/packetfifo.cpp: -------------------------------------------------------------------------------- 1 | #include "packetfifo.h" 2 | #include 3 | 4 | using namespace std; 5 | using namespace transcoding; 6 | 7 | StreamPacket::StreamPacket(AVPacket& packet, double timestamp) : 8 | packet(packet), timestamp(timestamp) { 9 | } 10 | 11 | StreamPacket::~StreamPacket() { 12 | } 13 | 14 | StreamPacketList::StreamPacketList() { 15 | } 16 | 17 | StreamPacketList::~StreamPacketList() { 18 | this->DropAllPackets(); 19 | } 20 | 21 | int StreamPacketList::GetCount() { 22 | return this->packets.size(); 23 | } 24 | 25 | void StreamPacketList::QueuePacket(AVPacket& packet, double timestamp) { 26 | // NOTE: assuming that packets in a stream come in order 27 | this->packets.push_back(new StreamPacket(packet, timestamp)); 28 | } 29 | 30 | double StreamPacketList::GetNextTimestamp() { 31 | if (this->packets.size()) { 32 | this->packets[0]->timestamp; 33 | } else { 34 | return -1; 35 | } 36 | } 37 | 38 | bool StreamPacketList::DequeuePacket(AVPacket& packet) { 39 | if (!this->packets.size()) { 40 | return false; 41 | } 42 | StreamPacket* streamPacket = this->packets.front(); 43 | this->packets.erase(this->packets.begin()); 44 | packet = streamPacket->packet; 45 | delete streamPacket; 46 | return true; 47 | } 48 | 49 | void StreamPacketList::DropAllPackets() { 50 | AVPacket packet; 51 | while (this->DequeuePacket(packet)) { 52 | av_free_packet(&packet); 53 | } 54 | } 55 | 56 | PacketFifo::PacketFifo(int streamCount) : 57 | count(0) { 58 | for (int n = 0; n < streamCount; n++) { 59 | this->streams.push_back(new StreamPacketList()); 60 | } 61 | } 62 | 63 | PacketFifo::~PacketFifo() { 64 | while(this->streams.size()) { 65 | StreamPacketList* list = this->streams.back(); 66 | this->streams.pop_back(); 67 | delete list; 68 | } 69 | } 70 | 71 | int PacketFifo::GetCount() { 72 | return this->count; 73 | } 74 | 75 | void PacketFifo::QueuePacket(int stream, AVPacket& packet, double timestamp) { 76 | this->count++; 77 | this->streams[stream]->QueuePacket(packet, timestamp); 78 | } 79 | 80 | bool PacketFifo::DequeuePacket(AVPacket& packet) { 81 | printf("dequeue: %d\n", this->count); 82 | int stream = -1; 83 | double lowestTimestamp = DBL_MAX; 84 | for (int n = 0; n < this->streams.size(); n++) { 85 | if (this->streams[n]->GetCount()) { 86 | double timestamp = this->streams[n]->GetNextTimestamp(); 87 | if (timestamp < lowestTimestamp) { 88 | stream = n; 89 | lowestTimestamp = timestamp; 90 | } 91 | } 92 | } 93 | if (stream == -1) { 94 | printf("no stream\n"); 95 | return false; 96 | } 97 | this->streams[stream]->DequeuePacket(packet); 98 | this->count--; 99 | return true; 100 | } 101 | 102 | void PacketFifo::DropAllPackets() { 103 | for (int n = 0; n < this->streams.size(); n++) { 104 | this->streams[n]->DropAllPackets(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/packetfifo.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "utils.h" 5 | 6 | #ifndef NODE_TRANSCODING_PACKETFIFO 7 | #define NODE_TRANSCODING_PACKETFIFO 8 | 9 | using namespace v8; 10 | 11 | namespace transcoding { 12 | 13 | class StreamPacket { 14 | public: 15 | StreamPacket(AVPacket& packet, double timestamp); 16 | ~StreamPacket(); 17 | 18 | double timestamp; 19 | AVPacket packet; 20 | }; 21 | 22 | class StreamPacketList { 23 | public: 24 | StreamPacketList(); 25 | ~StreamPacketList(); 26 | 27 | int GetCount(); 28 | 29 | void QueuePacket(AVPacket& packet, double timestamp); 30 | double GetNextTimestamp(); 31 | bool DequeuePacket(AVPacket& packet); 32 | void DropAllPackets(); 33 | 34 | private: 35 | std::vector packets; 36 | }; 37 | 38 | class PacketFifo { 39 | public: 40 | PacketFifo(int streamCount); 41 | ~PacketFifo(); 42 | 43 | int GetCount(); 44 | 45 | void QueuePacket(int stream, AVPacket& packet, double timestamp); 46 | bool DequeuePacket(AVPacket& packet); 47 | void DropAllPackets(); 48 | 49 | private: 50 | int count; 51 | std::vector streams; 52 | }; 53 | 54 | }; // transcoding 55 | 56 | #endif // NODE_TRANSCODING_PACKETFIFO 57 | -------------------------------------------------------------------------------- /src/profile.cpp: -------------------------------------------------------------------------------- 1 | #include "profile.h" 2 | 3 | using namespace transcoding; 4 | using namespace v8; 5 | 6 | CodecOptions::CodecOptions(Handle source) : 7 | codec(""), profileId(FF_PROFILE_UNKNOWN), profileLevel(FF_LEVEL_UNKNOWN), 8 | bitrate(0) { 9 | HandleScope scope; 10 | 11 | this->codec = V8GetString(source, "codec", this->codec); 12 | this->profileId = V8GetNumber(source, "profileId", this->profileId); 13 | this->profileLevel = V8GetNumber(source, "profileLevel", this->profileLevel); 14 | this->bitrate = V8GetNumber(source, "bitrate", this->bitrate); 15 | } 16 | 17 | CodecOptions::~CodecOptions() { 18 | } 19 | 20 | AudioCodecOptions::AudioCodecOptions(Handle source) : 21 | CodecOptions(source), channels(2), sampleRate(44100), sampleFormat("s16") { 22 | HandleScope scope; 23 | 24 | this->channels = V8GetNumber(source, "channels", this->channels); 25 | this->sampleRate = V8GetNumber(source, "sampleRate", this->sampleRate); 26 | this->sampleFormat = V8GetString(source, "sampleFormat", this->sampleFormat); 27 | } 28 | 29 | AudioCodecOptions::~AudioCodecOptions() { 30 | } 31 | 32 | VideoCodecOptions::VideoCodecOptions(Handle source) : 33 | CodecOptions(source) { 34 | HandleScope scope; 35 | } 36 | 37 | VideoCodecOptions::~VideoCodecOptions() { 38 | 39 | } 40 | 41 | Profile::Profile(Handle source) : 42 | name("unknown"), container("mpegts") { 43 | HandleScope scope; 44 | 45 | this->name = V8GetString(source, "name", this->name); 46 | 47 | Local options = 48 | Local::Cast(source->Get(String::New("options"))); 49 | 50 | this->container = V8GetString(options, "container", this->container); 51 | 52 | Local audio = Local::Cast(options->Get(String::New("audio"))); 53 | Local video = Local::Cast(options->Get(String::New("video"))); 54 | if (!audio.IsEmpty()) { 55 | if (audio->IsArray()) { 56 | Local audios = Local::Cast(audio); 57 | for (uint32_t n = 0; n < audios->Length(); n++) { 58 | this->audioCodecs.push_back(new AudioCodecOptions( 59 | Local::Cast(audios->Get(n)))); 60 | } 61 | } else { 62 | this->audioCodecs.push_back(new AudioCodecOptions(audio)); 63 | } 64 | } 65 | if (!video.IsEmpty()) { 66 | if (video->IsArray()) { 67 | Local videos = Local::Cast(video); 68 | for (uint32_t n = 0; n < videos->Length(); n++) { 69 | this->videoCodecs.push_back(new VideoCodecOptions( 70 | Local::Cast(videos->Get(n)))); 71 | } 72 | } else { 73 | this->videoCodecs.push_back(new VideoCodecOptions(video)); 74 | } 75 | } 76 | } 77 | 78 | Profile::~Profile() { 79 | while (!this->audioCodecs.empty()) { 80 | delete this->audioCodecs.back(); 81 | this->audioCodecs.pop_back(); 82 | } 83 | while (!this->videoCodecs.empty()) { 84 | delete this->videoCodecs.back(); 85 | this->videoCodecs.pop_back(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/profile.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "utils.h" 6 | 7 | #ifndef NODE_TRANSCODING_PROFILE 8 | #define NODE_TRANSCODING_PROFILE 9 | 10 | using namespace v8; 11 | 12 | namespace transcoding { 13 | 14 | class CodecOptions { 15 | public: 16 | CodecOptions(Handle source); 17 | virtual ~CodecOptions(); 18 | 19 | std::string codec; 20 | int profileId; 21 | int profileLevel; 22 | int bitrate; 23 | }; 24 | 25 | class AudioCodecOptions : public CodecOptions { 26 | public: 27 | AudioCodecOptions(Handle source); 28 | virtual ~AudioCodecOptions(); 29 | 30 | int channels; 31 | int sampleRate; 32 | std::string sampleFormat; 33 | }; 34 | 35 | class VideoCodecOptions : public CodecOptions { 36 | public: 37 | VideoCodecOptions(Handle source); 38 | virtual ~VideoCodecOptions(); 39 | }; 40 | 41 | class Profile { 42 | public: 43 | Profile(Handle source); 44 | ~Profile(); 45 | 46 | std::string name; 47 | 48 | std::string container; 49 | 50 | std::vector audioCodecs; 51 | std::vector videoCodecs; 52 | }; 53 | 54 | }; // transcoding 55 | 56 | #endif // NODE_TRANSCODING_PROFILE 57 | -------------------------------------------------------------------------------- /src/query.cpp: -------------------------------------------------------------------------------- 1 | #include "query.h" 2 | #include "mediainfo.h" 3 | 4 | using namespace transcoding; 5 | using namespace transcoding::io; 6 | 7 | static Persistent _query_ctor; 8 | 9 | void Query::Init(Handle target) { 10 | HandleScope scope; 11 | 12 | Local ctor = FunctionTemplate::New(New); 13 | ctor->InstanceTemplate()->SetInternalFieldCount(1); 14 | ctor->SetClassName(String::NewSymbol("Query")); 15 | 16 | NODE_SET_PROTOTYPE_METHOD(ctor, "start", Start); 17 | NODE_SET_PROTOTYPE_METHOD(ctor, "stop", Stop); 18 | 19 | NODE_SET_PROTOTYPE_ACCESSOR(ctor, "source", GetSource); 20 | 21 | _query_ctor = Persistent::New(ctor); 22 | target->Set(String::NewSymbol("Query"), _query_ctor->GetFunction()); 23 | } 24 | 25 | Handle Query::New(const Arguments& args) { 26 | HandleScope scope; 27 | Local source = args[0]->ToObject(); 28 | Query* query = new Query(source); 29 | query->Wrap(args.This()); 30 | return scope.Close(args.This()); 31 | } 32 | 33 | Query::Query(Handle source) : 34 | context(NULL), abort(false), err(0) { 35 | TC_LOG_D("Query::Query()\n"); 36 | HandleScope scope; 37 | 38 | this->source = Persistent::New(source); 39 | 40 | pthread_mutex_init(&this->lock, NULL); 41 | 42 | this->asyncReq = new uv_async_t(); 43 | this->asyncReq->data = this; 44 | uv_async_init(uv_default_loop(), this->asyncReq, CompleteAsync); 45 | } 46 | 47 | Query::~Query() { 48 | TC_LOG_D("Query::~Query()\n"); 49 | HandleScope scope; 50 | assert(!this->context); 51 | 52 | pthread_mutex_destroy(&this->lock); 53 | 54 | this->source.Dispose(); 55 | } 56 | 57 | Handle Query::GetSource(Local property, 58 | const AccessorInfo& info) { 59 | HandleScope scope; 60 | Query* query = ObjectWrap::Unwrap(info.This()); 61 | return scope.Close(query->source); 62 | } 63 | 64 | Handle Query::Start(const Arguments& args) { 65 | TC_LOG_D("Query::Start()\n"); 66 | 67 | HandleScope scope; 68 | Query* query = ObjectWrap::Unwrap(args.This()); 69 | 70 | assert(!query->context); 71 | 72 | // Since we are just a query, only read small chunks - if sourcing from the 73 | // network this will greatly reduce the chance of use downloading entire files 74 | // just to read the headers 75 | size_t maxBufferBytes = 128 * 1024; 76 | IOReader* input = IOReader::Create(query->source, maxBufferBytes); 77 | 78 | QueryContext* context = new QueryContext(input); 79 | 80 | // Prepare thread request 81 | uv_work_t* req = new uv_work_t(); 82 | req->data = query; 83 | query->Ref(); 84 | query->context = context; 85 | 86 | // Start thread 87 | int status = uv_queue_work(uv_default_loop(), req, 88 | ThreadWorker, ThreadWorkerAfter); 89 | assert(status == 0); 90 | 91 | return scope.Close(Undefined()); 92 | } 93 | 94 | Handle Query::Stop(const Arguments& args) { 95 | TC_LOG_D("Query::Stop()\n"); 96 | 97 | HandleScope scope; 98 | Query* query = ObjectWrap::Unwrap(args.This()); 99 | 100 | pthread_mutex_lock(&query->lock); 101 | query->abort = true; 102 | pthread_mutex_unlock(&query->lock); 103 | 104 | return scope.Close(Undefined()); 105 | } 106 | 107 | void Query::EmitInfo(Handle sourceInfo) { 108 | HandleScope scope; 109 | 110 | Handle argv[] = { 111 | String::New("info"), 112 | sourceInfo, 113 | }; 114 | node::MakeCallback(this->handle_, "emit", countof(argv), argv); 115 | } 116 | 117 | void Query::EmitError(int err) { 118 | HandleScope scope; 119 | 120 | char buffer[256]; 121 | av_strerror(err, buffer, sizeof(buffer)); 122 | 123 | Handle argv[] = { 124 | String::New("error"), 125 | Exception::Error(String::New(buffer)), 126 | }; 127 | node::MakeCallback(this->handle_, "emit", countof(argv), argv); 128 | } 129 | 130 | void Query::CompleteAsync(uv_async_t* handle, int status) { 131 | HandleScope scope; 132 | Query* query = static_cast(handle->data); 133 | if (!query) { 134 | return; 135 | } 136 | TC_LOG_D("Query::CompleteAsync(): err %d\n", query->err); 137 | QueryContext* context = query->context; 138 | assert(context); 139 | 140 | int err = query->err; 141 | 142 | Local sourceInfo; 143 | if (!err) { 144 | sourceInfo = Local::New(createMediaInfo(context->ictx, false)); 145 | } 146 | 147 | delete query->context; 148 | query->context = NULL; 149 | 150 | if (err) { 151 | query->EmitError(err); 152 | } else { 153 | query->EmitInfo(sourceInfo); 154 | } 155 | 156 | query->Unref(); 157 | handle->data = NULL; // no more 158 | 159 | uv_close((uv_handle_t*)handle, AsyncHandleClose); 160 | } 161 | 162 | void Query::AsyncHandleClose(uv_handle_t* handle) { 163 | TC_LOG_D("Query::AsyncHandleClose()\n"); 164 | delete handle; 165 | } 166 | 167 | void Query::ThreadWorker(uv_work_t* request) { 168 | TC_LOG_D("Query::ThreadWorker()\n"); 169 | 170 | Query* query = static_cast(request->data); 171 | QueryContext* context = query->context; 172 | assert(context); 173 | 174 | int ret = context->Execute(); 175 | if (ret) { 176 | TC_LOG_D("Query::ThreadWorker(): execute failed (%d)\n", ret); 177 | query->err = ret; 178 | } 179 | 180 | // Complete 181 | // Note that we fire this instead of doing it in the worker complete so that 182 | // all progress events will get dispatched prior to this 183 | uv_async_send(query->asyncReq); 184 | 185 | TC_LOG_D("Query::ThreadWorker() exiting\n"); 186 | } 187 | 188 | void Query::ThreadWorkerAfter(uv_work_t* request) { 189 | delete request; 190 | } 191 | -------------------------------------------------------------------------------- /src/query.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "utils.h" 4 | #include "querycontext.h" 5 | #include "io/io.h" 6 | 7 | #ifndef NODE_TRANSCODING_QUERY 8 | #define NODE_TRANSCODING_QUERY 9 | 10 | using namespace v8; 11 | 12 | namespace transcoding { 13 | 14 | class Query : public node::ObjectWrap { 15 | public: 16 | static void Init(Handle target); 17 | static Handle New(const Arguments& args); 18 | 19 | public: 20 | Query(Handle source); 21 | ~Query(); 22 | 23 | static Handle GetSource(Local property, 24 | const AccessorInfo& info); 25 | 26 | static Handle Start(const Arguments& args); 27 | static Handle Stop(const Arguments& args); 28 | 29 | public: 30 | void EmitInfo(Handle sourceInfo); 31 | void EmitError(int err); 32 | 33 | static void CompleteAsync(uv_async_t* handle, int status); 34 | static void AsyncHandleClose(uv_handle_t* handle); 35 | 36 | static void ThreadWorker(uv_work_t* request); 37 | static void ThreadWorkerAfter(uv_work_t* request); 38 | 39 | private: 40 | Persistent source; 41 | 42 | QueryContext* context; 43 | 44 | pthread_mutex_t lock; 45 | bool abort; 46 | int err; 47 | 48 | uv_async_t* asyncReq; 49 | }; 50 | 51 | }; // transcoding 52 | 53 | #endif // NODE_TRANSCODING_QUERY 54 | -------------------------------------------------------------------------------- /src/querycontext.cpp: -------------------------------------------------------------------------------- 1 | #include "querycontext.h" 2 | 3 | using namespace transcoding; 4 | using namespace transcoding::io; 5 | 6 | QueryContext::QueryContext(IOReader* input) : 7 | input(input), 8 | ictx(NULL) { 9 | } 10 | 11 | QueryContext::~QueryContext() { 12 | if (this->ictx) { 13 | avformat_free_context(this->ictx); 14 | } 15 | 16 | IOHandle::CloseWhenDone(this->input); 17 | } 18 | 19 | int QueryContext::Execute() { 20 | int ret = 0; 21 | 22 | // Grab contexts 23 | AVFormatContext* ictx = NULL; 24 | if (!ret) { 25 | ictx = createInputContext(this->input, &ret); 26 | if (ret) { 27 | if (ictx) { 28 | avformat_free_context(ictx); 29 | } 30 | ictx = NULL; 31 | } 32 | } 33 | 34 | if (!ret) { 35 | this->ictx = ictx; 36 | } else { 37 | if (ictx) { 38 | avformat_free_context(ictx); 39 | } 40 | ictx = NULL; 41 | } 42 | 43 | return ret; 44 | } 45 | -------------------------------------------------------------------------------- /src/querycontext.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "utils.h" 4 | #include "io/io.h" 5 | 6 | #ifndef NODE_TRANSCODING_QUERYCONTEXT 7 | #define NODE_TRANSCODING_QUERYCONTEXT 8 | 9 | using namespace v8; 10 | 11 | namespace transcoding { 12 | 13 | class QueryContext { 14 | public: 15 | QueryContext(io::IOReader* input); 16 | ~QueryContext(); 17 | 18 | // Occurs exclusively in the v8 thread 19 | int Execute(); 20 | 21 | public: 22 | io::IOReader* input; 23 | 24 | AVFormatContext* ictx; 25 | }; 26 | 27 | }; // transcoding 28 | 29 | #endif // NODE_TRANSCODING_QUERYCONTEXT 30 | -------------------------------------------------------------------------------- /src/task.cpp: -------------------------------------------------------------------------------- 1 | #include "task.h" 2 | #include "mediainfo.h" 3 | 4 | using namespace transcoding; 5 | using namespace transcoding::io; 6 | 7 | static Persistent _task_timestamp_symbol; 8 | static Persistent _task_duration_symbol; 9 | static Persistent _task_timeElapsed_symbol; 10 | static Persistent _task_timeEstimated_symbol; 11 | static Persistent _task_timeRemaining_symbol; 12 | static Persistent _task_timeMultiplier_symbol; 13 | 14 | static Persistent _task_ctor; 15 | 16 | void Task::Init(Handle target) { 17 | HandleScope scope; 18 | 19 | _task_timestamp_symbol = NODE_PSYMBOL("timestamp"); 20 | _task_duration_symbol = NODE_PSYMBOL("duration"); 21 | _task_timeElapsed_symbol = NODE_PSYMBOL("timeElapsed"); 22 | _task_timeEstimated_symbol = NODE_PSYMBOL("timeEstimated"); 23 | _task_timeRemaining_symbol = NODE_PSYMBOL("timeRemaining"); 24 | _task_timeMultiplier_symbol = NODE_PSYMBOL("timeMultiplier"); 25 | 26 | Local ctor = FunctionTemplate::New(New); 27 | ctor->InstanceTemplate()->SetInternalFieldCount(1); 28 | ctor->SetClassName(String::NewSymbol("Task")); 29 | 30 | NODE_SET_PROTOTYPE_METHOD(ctor, "start", Start); 31 | NODE_SET_PROTOTYPE_METHOD(ctor, "stop", Stop); 32 | 33 | NODE_SET_PROTOTYPE_ACCESSOR(ctor, "source", GetSource); 34 | NODE_SET_PROTOTYPE_ACCESSOR(ctor, "target", GetTarget); 35 | NODE_SET_PROTOTYPE_ACCESSOR(ctor, "profile", GetProfile); 36 | NODE_SET_PROTOTYPE_ACCESSOR(ctor, "options", GetOptions); 37 | NODE_SET_PROTOTYPE_ACCESSOR(ctor, "progress", GetProgress); 38 | 39 | _task_ctor = Persistent::New(ctor); 40 | target->Set(String::NewSymbol("Task"), _task_ctor->GetFunction()); 41 | } 42 | 43 | Handle Task::New(const Arguments& args) { 44 | HandleScope scope; 45 | Task* task = new Task( 46 | args[0]->ToObject(), 47 | args[1], 48 | args[2]->ToObject(), 49 | args[3]); 50 | task->Wrap(args.This()); 51 | return scope.Close(args.This()); 52 | } 53 | 54 | Task::Task(Handle source, Handle targetValue, 55 | Handle profile, Handle optionsValue) : 56 | context(NULL), abort(false), err(0) { 57 | TC_LOG_D("Task::Task()\n"); 58 | HandleScope scope; 59 | 60 | this->source = Persistent::New(source); 61 | if (targetValue.IsEmpty() || targetValue->IsNull()) { 62 | // Null target 63 | } else { 64 | this->target = Persistent::New(targetValue->ToObject()); 65 | } 66 | this->profile = Persistent::New(profile); 67 | if (optionsValue.IsEmpty() || optionsValue->IsNull()) { 68 | this->options = Persistent::New(Object::New()); 69 | } else { 70 | this->options = Persistent::New(optionsValue->ToObject()); 71 | } 72 | 73 | memset(&this->progress, 0, sizeof(this->progress)); 74 | 75 | this->asyncReq = new uv_async_t(); 76 | this->asyncReq->data = this; 77 | uv_async_init(uv_default_loop(), this->asyncReq, ProcessAsync); 78 | 79 | pthread_mutex_init(&this->lock, NULL); 80 | } 81 | 82 | Task::~Task() { 83 | TC_LOG_D("Task::~Task()\n"); 84 | HandleScope scope; 85 | assert(!this->context); 86 | 87 | if (this->messages.size()) { 88 | TC_LOG_D("Task::~Task(): dtor when messages %d pending\n", 89 | (int)this->messages.size()); 90 | } 91 | pthread_mutex_destroy(&this->lock); 92 | 93 | this->source.Dispose(); 94 | this->target.Dispose(); 95 | this->profile.Dispose(); 96 | this->options.Dispose(); 97 | } 98 | 99 | Handle Task::GetSource(Local property, 100 | const AccessorInfo& info) { 101 | HandleScope scope; 102 | Task* task = ObjectWrap::Unwrap(info.This()); 103 | return scope.Close(task->source); 104 | } 105 | 106 | Handle Task::GetTarget(Local property, 107 | const AccessorInfo& info) { 108 | HandleScope scope; 109 | Task* task = ObjectWrap::Unwrap(info.This()); 110 | return scope.Close(task->target); 111 | } 112 | 113 | Handle Task::GetProfile(Local property, 114 | const AccessorInfo& info) { 115 | HandleScope scope; 116 | Task* task = ObjectWrap::Unwrap(info.This()); 117 | return scope.Close(task->profile); 118 | } 119 | 120 | Handle Task::GetOptions(Local property, 121 | const AccessorInfo& info) { 122 | HandleScope scope; 123 | Task* task = ObjectWrap::Unwrap(info.This()); 124 | return scope.Close(task->options); 125 | } 126 | 127 | Handle Task::GetProgressInternal(Progress* progress) { 128 | HandleScope scope; 129 | 130 | Local result = Object::New(); 131 | 132 | result->Set(_task_timestamp_symbol, 133 | Number::New(progress->timestamp)); 134 | result->Set(_task_duration_symbol, 135 | Number::New(progress->duration)); 136 | result->Set(_task_timeElapsed_symbol, 137 | Number::New(progress->timeElapsed)); 138 | result->Set(_task_timeEstimated_symbol, 139 | Number::New(progress->timeEstimated)); 140 | result->Set(_task_timeRemaining_symbol, 141 | Number::New(progress->timeRemaining)); 142 | result->Set(_task_timeMultiplier_symbol, 143 | Number::New(progress->timeMultiplier)); 144 | 145 | return scope.Close(result); 146 | } 147 | 148 | Handle Task::GetProgress(Local property, 149 | const AccessorInfo& info) { 150 | HandleScope scope; 151 | Task* task = ObjectWrap::Unwrap(info.This()); 152 | 153 | if (task->context) { 154 | return scope.Close(task->GetProgressInternal(&task->progress)); 155 | } else { 156 | return scope.Close(Null()); 157 | } 158 | } 159 | 160 | Handle Task::Start(const Arguments& args) { 161 | TC_LOG_D("Task::Start()\n"); 162 | HandleScope scope; 163 | Task* task = ObjectWrap::Unwrap(args.This()); 164 | 165 | assert(!task->context); 166 | 167 | Profile* profile = new Profile(task->profile); 168 | TaskOptions* options = new TaskOptions(task->options); 169 | if (options->liveStreaming) { 170 | // Must force to MPEGTS container 171 | TC_LOG_D("Task::Start(): HTTP Live Streaming enabled, forcing to MPEGTS\n"); 172 | profile->container = "mpegts"; 173 | } 174 | 175 | // Ready input 176 | IOReader* input = IOReader::Create(task->source); 177 | 178 | // Setup context 179 | TaskContext* context = NULL; 180 | if (options->liveStreaming) { 181 | context = new LiveStreamingTaskContext(input, profile, options); 182 | } else { 183 | IOWriter* output = IOWriter::Create(task->target); 184 | context = new SingleFileTaskContext(input, output, profile, options); 185 | } 186 | 187 | // Prepare thread request 188 | uv_work_t* req = new uv_work_t(); 189 | req->data = task; 190 | task->Ref(); 191 | task->context = context; 192 | 193 | // Start thread 194 | int status = uv_queue_work(uv_default_loop(), req, 195 | ThreadWorker, ThreadWorkerAfter); 196 | assert(status == 0); 197 | 198 | return scope.Close(Undefined()); 199 | } 200 | 201 | Handle Task::Stop(const Arguments& args) { 202 | TC_LOG_D("Task::Stop()\n"); 203 | HandleScope scope; 204 | Task* task = ObjectWrap::Unwrap(args.This()); 205 | 206 | pthread_mutex_lock(&task->lock); 207 | task->abort = true; 208 | pthread_mutex_unlock(&task->lock); 209 | 210 | return scope.Close(Undefined()); 211 | } 212 | 213 | void Task::EmitBegin(AVFormatContext* ictx, AVFormatContext* octx) { 214 | HandleScope scope; 215 | 216 | Local sourceInfo = Local::New(createMediaInfo(ictx, false)); 217 | Local targetInfo = Local::New(createMediaInfo(octx, true)); 218 | 219 | Handle argv[] = { 220 | String::New("begin"), 221 | sourceInfo, 222 | targetInfo, 223 | }; 224 | node::MakeCallback(this->handle_, "emit", countof(argv), argv); 225 | } 226 | 227 | void Task::EmitProgress(Progress progress) { 228 | HandleScope scope; 229 | 230 | Handle argv[] = { 231 | String::New("progress"), 232 | this->GetProgressInternal(&progress), 233 | }; 234 | node::MakeCallback(this->handle_, "emit", countof(argv), argv); 235 | } 236 | 237 | void Task::EmitError(int err) { 238 | HandleScope scope; 239 | 240 | char buffer[256]; 241 | av_strerror(err, buffer, sizeof(buffer)); 242 | 243 | Handle argv[] = { 244 | String::New("error"), 245 | Exception::Error(String::New(buffer)), 246 | }; 247 | node::MakeCallback(this->handle_, "emit", countof(argv), argv); 248 | } 249 | 250 | void Task::EmitEnd() { 251 | HandleScope scope; 252 | 253 | Handle argv[] = { 254 | String::New("end"), 255 | }; 256 | node::MakeCallback(this->handle_, "emit", countof(argv), argv); 257 | } 258 | 259 | void Task::ProcessAsync(uv_async_t* handle, int status) { 260 | HandleScope scope; 261 | assert(status == 0); 262 | Task* task = static_cast(handle->data); 263 | if (!task) { 264 | return; 265 | } 266 | 267 | TaskContext* context = task->context; 268 | assert(context); 269 | 270 | while (true) { 271 | int abortAll = false; 272 | 273 | pthread_mutex_lock(&task->lock); 274 | TaskMessage* message = NULL; 275 | int remaining = task->messages.size(); 276 | if (remaining) { 277 | message = task->messages.front(); 278 | task->messages.erase(task->messages.begin()); 279 | } 280 | pthread_mutex_unlock(&task->lock); 281 | if (!message) { 282 | break; 283 | } 284 | 285 | switch (message->type) { 286 | case TaskMessageBegin: 287 | TC_LOG_D("Task::ProcessAsync(TaskMessageBegin)\n"); 288 | // NOTE: NOT THREAD SAFE AGHHHH 289 | task->EmitBegin(context->ictx, context->octx); 290 | break; 291 | case TaskMessageProgress: 292 | TC_LOG_D("Task::ProcessAsync(TaskMessageProgress)\n"); 293 | task->progress = message->progress; 294 | task->EmitProgress(message->progress); 295 | break; 296 | case TaskMessageComplete: 297 | TC_LOG_D("Task::ProcessAsync(TaskMessageComplete), err: %d\n", task->err); 298 | // Always fire one last progress event 299 | if (!task->err) { 300 | task->progress.timestamp = task->progress.duration; 301 | task->progress.timeRemaining = 0; 302 | } 303 | task->EmitProgress(task->progress); 304 | 305 | int err = task->err; 306 | 307 | delete task->context; 308 | task->context = NULL; 309 | 310 | if (err) { 311 | task->EmitError(err); 312 | } else { 313 | task->EmitEnd(); 314 | } 315 | 316 | uv_close((uv_handle_t*)task->asyncReq, AsyncHandleClose); 317 | 318 | task->Unref(); 319 | handle->data = NULL; // no more processing! 320 | abortAll = true; 321 | break; 322 | } 323 | 324 | delete message; 325 | } 326 | } 327 | 328 | void Task::AsyncHandleClose(uv_handle_t* handle) { 329 | TC_LOG_D("Task::AsyncHandleClose()\n"); 330 | delete handle; 331 | } 332 | 333 | #define EMIT_PROGRESS_TIME_CAP 1.0 // sec between emits 334 | #define EMIT_PROGRESS_PERCENT_CAP 0.01 // 1/100*% between emits 335 | 336 | void Task::ThreadWorker(uv_work_t* request) { 337 | TC_LOG_D("Task::ThreadWorker()\n"); 338 | 339 | Task* task = static_cast(request->data); 340 | TaskContext* context = task->context; 341 | assert(context); 342 | 343 | TaskMessage* msg; 344 | 345 | // Prepare the input/output (done on the main thread to make things easier) 346 | int ret = context->PrepareInput(); 347 | if (!ret) { 348 | ret = context->PrepareOutput(); 349 | } 350 | if (ret) { 351 | TC_LOG_D("Task::ThreadWorker(): Prepare failed (%d)\n", ret); 352 | pthread_mutex_lock(&task->lock); 353 | task->err = ret; 354 | task->messages.push_back(new TaskMessage(TaskMessageComplete)); 355 | pthread_mutex_unlock(&task->lock); 356 | uv_async_send(task->asyncReq); 357 | return; 358 | } 359 | 360 | // Begin 361 | pthread_mutex_lock(&task->lock); 362 | task->messages.push_back(new TaskMessage(TaskMessageBegin)); 363 | pthread_mutex_unlock(&task->lock); 364 | uv_async_send(task->asyncReq); 365 | 366 | double percentDelta = 0; 367 | int64_t startTime = av_gettime(); 368 | int64_t lastProgressTime = 0; 369 | Progress progress; 370 | memset(&progress, 0, sizeof(progress)); 371 | progress.duration = context->ictx->duration / (double)AV_TIME_BASE; 372 | 373 | bool aborting = false; 374 | do { 375 | // Grab the current abort flag 376 | pthread_mutex_lock(&task->lock); 377 | aborting = task->abort; 378 | pthread_mutex_unlock(&task->lock); 379 | if (aborting) { 380 | TC_LOG_D("Task::ThreadWorker(): aborting\n"); 381 | break; 382 | } 383 | 384 | // Emit progress event, if needed 385 | int64_t currentTime = av_gettime(); 386 | bool emitProgress = 387 | (currentTime - lastProgressTime > EMIT_PROGRESS_TIME_CAP * 1000000) || 388 | (percentDelta > EMIT_PROGRESS_PERCENT_CAP); 389 | if (emitProgress) { 390 | lastProgressTime = currentTime; 391 | percentDelta = 0; 392 | 393 | // Progress 394 | TaskMessage* msg = new TaskMessage(TaskMessageProgress); 395 | msg->progress = progress; 396 | pthread_mutex_lock(&task->lock); 397 | task->messages.push_back(msg); 398 | // Quick optimization so we don't flood requests 399 | bool needsAsyncReq = task->messages.size() == 1; 400 | pthread_mutex_unlock(&task->lock); 401 | if (needsAsyncReq) { 402 | uv_async_send(task->asyncReq); 403 | } 404 | } 405 | 406 | // Perform some work 407 | double oldPercent = progress.timestamp / progress.duration; 408 | bool finished = context->Pump(&ret, &progress); 409 | percentDelta += (progress.timestamp / progress.duration) - oldPercent; 410 | task->err = ret; 411 | 412 | // End, if needed 413 | if (finished && !ret) { 414 | TC_LOG_D("Task::ThreadWorker(): end, err: %d, ret: %d, finished: %d\n", 415 | task->err, ret, finished); 416 | context->End(); 417 | break; 418 | } 419 | } while (!ret && !aborting); 420 | 421 | TC_LOG_D("Task::ThreadWorker(): done (%d)\n", task->err); 422 | 423 | // Complete 424 | // Note that we fire this instead of doing it in the worker complete so that 425 | // all progress events will get dispatched prior to this 426 | pthread_mutex_lock(&task->lock); 427 | task->messages.push_back(new TaskMessage(TaskMessageComplete)); 428 | pthread_mutex_unlock(&task->lock); 429 | uv_async_send(task->asyncReq); 430 | 431 | TC_LOG_D("Task::ThreadWorker() exiting\n"); 432 | } 433 | 434 | void Task::ThreadWorkerAfter(uv_work_t* request) { 435 | delete request; 436 | } 437 | -------------------------------------------------------------------------------- /src/task.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "utils.h" 5 | #include "profile.h" 6 | #include "taskcontext.h" 7 | #include "io/io.h" 8 | 9 | #ifndef NODE_TRANSCODING_TASK 10 | #define NODE_TRANSCODING_TASK 11 | 12 | using namespace v8; 13 | 14 | namespace transcoding { 15 | 16 | typedef enum TaskMessageType_e { 17 | TaskMessageBegin, 18 | TaskMessageProgress, 19 | TaskMessageComplete, 20 | } TaskMessageType; 21 | 22 | class TaskMessage { 23 | public: 24 | TaskMessage(TaskMessageType type) : type(type) {} 25 | TaskMessageType type; 26 | union { 27 | Progress progress; 28 | }; 29 | }; 30 | 31 | class Task : public node::ObjectWrap { 32 | public: 33 | static void Init(Handle target); 34 | static Handle New(const Arguments& args); 35 | 36 | public: 37 | Task(Handle source, Handle targetValue, 38 | Handle profile, Handle optionsValue); 39 | ~Task(); 40 | 41 | static Handle GetSource(Local property, 42 | const AccessorInfo& info); 43 | static Handle GetTarget(Local property, 44 | const AccessorInfo& info); 45 | static Handle GetProfile(Local property, 46 | const AccessorInfo& info); 47 | static Handle GetOptions(Local property, 48 | const AccessorInfo& info); 49 | static Handle GetProgress(Local property, 50 | const AccessorInfo& info); 51 | 52 | static Handle Start(const Arguments& args); 53 | static Handle Stop(const Arguments& args); 54 | 55 | public: 56 | void EmitBegin(AVFormatContext* ictx, AVFormatContext* octx); 57 | void EmitProgress(Progress progress); 58 | void EmitError(int err); 59 | void EmitEnd(); 60 | 61 | static void ProcessAsync(uv_async_t* handle, int status); 62 | static void AsyncHandleClose(uv_handle_t* handle); 63 | 64 | static void ThreadWorker(uv_work_t* request); 65 | static void ThreadWorkerAfter(uv_work_t* request); 66 | 67 | private: 68 | Handle GetProgressInternal(Progress* progress); 69 | 70 | private: 71 | Persistent source; 72 | Persistent target; 73 | Persistent profile; 74 | Persistent options; 75 | 76 | TaskContext* context; 77 | Progress progress; 78 | 79 | uv_async_t* asyncReq; 80 | pthread_mutex_t lock; 81 | bool abort; 82 | int err; 83 | std::vector messages; 84 | }; 85 | 86 | }; // transcoding 87 | 88 | #endif // NODE_TRANSCODING_TASK 89 | -------------------------------------------------------------------------------- /src/taskcontext.cpp: -------------------------------------------------------------------------------- 1 | #include "taskcontext.h" 2 | 3 | using namespace transcoding; 4 | using namespace transcoding::io; 5 | 6 | // Number of packets to keep in the FIFO 7 | #define FIFO_MIN_COUNT 64 8 | 9 | TaskContext::TaskContext(IOReader* input, Profile* profile, 10 | TaskOptions* options) : 11 | input(input), profile(profile), options(options), 12 | ictx(NULL), bitStreamFilter(NULL), fifo(NULL), doneReading(false) { 13 | TC_LOG_D("TaskContext::TaskContext()\n"); 14 | } 15 | 16 | TaskContext::~TaskContext() { 17 | TC_LOG_D("TaskContext::~TaskContext()\n"); 18 | 19 | if (this->fifo) { 20 | delete this->fifo; 21 | } 22 | 23 | if (this->bitStreamFilter) { 24 | av_bitstream_filter_close(this->bitStreamFilter); 25 | } 26 | 27 | if (this->ictx) { 28 | avformat_free_context(this->ictx); 29 | } 30 | 31 | delete this->profile; 32 | delete this->options; 33 | 34 | IOHandle::CloseWhenDone(this->input); 35 | } 36 | 37 | int TaskContext::PrepareInput() { 38 | TC_LOG_D("TaskContext::PrepareInput()\n"); 39 | int ret = 0; 40 | 41 | AVFormatContext* ictx = createInputContext(this->input, &ret); 42 | if (ret) { 43 | TC_LOG_D("TaskContext::PrepareInput(): failed createInputContext (%d)\n", 44 | ret); 45 | } 46 | 47 | if (!ret) { 48 | this->ictx = ictx; 49 | } else { 50 | if (ictx) { 51 | avformat_free_context(ictx); 52 | } 53 | ictx = NULL; 54 | } 55 | 56 | TC_LOG_D("TaskContext::PrepareInput() = %d\n", ret); 57 | return ret; 58 | } 59 | 60 | int TaskContext::PrepareOutput() { 61 | TC_LOG_D("TaskContext::PrepareOutput()\n"); 62 | int ret = 0; 63 | 64 | AVFormatContext* octx = avformat_alloc_context(); 65 | 66 | // Setup output container 67 | if (!ret) { 68 | AVOutputFormat* ofmt = av_guess_format( 69 | profile->container.c_str(), NULL, NULL); 70 | if (ofmt) { 71 | octx->oformat = ofmt; 72 | } else { 73 | ret = AVERROR_NOFMT; 74 | TC_LOG_D("TaskContext::PrepareOutput(): oformat %s not found (%d)\n", 75 | profile->container.c_str(), ret); 76 | } 77 | octx->duration = ictx->duration; 78 | octx->start_time = ictx->start_time; 79 | octx->bit_rate = ictx->bit_rate; 80 | } 81 | 82 | // Setup input FIFO 83 | if (!ret) { 84 | this->fifo = new PacketFifo(ictx->nb_streams); 85 | } 86 | 87 | // Setup streams 88 | if (!ret) { 89 | for (int n = 0; n < ictx->nb_streams; n++) { 90 | AVStream* stream = ictx->streams[n]; 91 | switch (stream->codec->codec_type) { 92 | case CODEC_TYPE_VIDEO: 93 | case CODEC_TYPE_AUDIO: 94 | stream->discard = AVDISCARD_NONE; 95 | this->AddOutputStreamCopy(octx, stream, &ret); 96 | break; 97 | // TODO: subtitles 98 | case CODEC_TYPE_SUBTITLE: 99 | default: 100 | stream->discard = AVDISCARD_ALL; 101 | break; 102 | } 103 | if (ret) { 104 | TC_LOG_D("TaskContext::PrepareOutput(): failed stream add (%d)\n", ret); 105 | break; 106 | } 107 | } 108 | } 109 | 110 | // Scan video streams to see if we need to set up the bitstream filter for 111 | // fixing h264 in mpegts 112 | // This is equivalent to the -vbsf h264_mp4toannexb option 113 | if (!ret && strcmp(octx->oformat->name, "mpegts") == 0) { 114 | for (int n = 0; n < octx->nb_streams; n++) { 115 | AVStream* stream = octx->streams[n]; 116 | if (stream->codec->codec_id == CODEC_ID_H264) { 117 | TC_LOG_D("TaskContext::PrepareOutput(): h264_mp4toannexb on stream " 118 | "%d\n", n); 119 | AVBitStreamFilterContext* bsfc = 120 | av_bitstream_filter_init("h264_mp4toannexb"); 121 | if (!bsfc) { 122 | ret = AVERROR_BSF_NOT_FOUND; 123 | TC_LOG_D("TaskContext::PrepareOutput(): h264_mp4toannexb missing\n"); 124 | } else { 125 | bsfc->next = this->bitStreamFilter; 126 | this->bitStreamFilter = bsfc; 127 | } 128 | } 129 | } 130 | } 131 | 132 | if (!ret) { 133 | this->octx = octx; 134 | } else { 135 | if (octx) { 136 | avformat_free_context(octx); 137 | } 138 | octx = NULL; 139 | } 140 | 141 | TC_LOG_D("TaskContext::PrepareOutput() = %d\n", ret); 142 | return ret; 143 | } 144 | 145 | AVStream* TaskContext::AddOutputStreamCopy(AVFormatContext* octx, 146 | AVStream* istream, int* pret) { 147 | int ret = 0; 148 | AVStream* ostream = NULL; 149 | AVCodec* codec = NULL; 150 | AVCodecContext* icodec = NULL; 151 | AVCodecContext* ocodec = NULL; 152 | 153 | codec = avcodec_find_encoder(istream->codec->codec_id); 154 | if (!codec) { 155 | ret = AVERROR_ENCODER_NOT_FOUND; 156 | goto CLEANUP; 157 | } 158 | 159 | ostream = av_new_stream(octx, 0); 160 | if (!ostream) { 161 | ret = AVERROR_NOMEM; 162 | goto CLEANUP; 163 | } 164 | 165 | icodec = istream->codec; 166 | ocodec = ostream->codec; 167 | 168 | // May not do anything 169 | ostream->stream_copy = 1; 170 | 171 | avcodec_get_context_defaults3(ocodec, codec); 172 | 173 | ocodec->codec_id = icodec->codec_id; 174 | ocodec->codec_type = icodec->codec_type; 175 | ocodec->codec_tag = icodec->codec_tag; 176 | ocodec->profile = icodec->profile; 177 | ocodec->level = icodec->level; 178 | ocodec->bit_rate = icodec->bit_rate; 179 | ocodec->bits_per_raw_sample = icodec->bits_per_raw_sample; 180 | ocodec->chroma_sample_location = icodec->chroma_sample_location; 181 | ocodec->rc_max_rate = icodec->rc_max_rate; 182 | ocodec->rc_buffer_size = icodec->rc_buffer_size; 183 | 184 | // Input stream may not end on frame boundaries 185 | if (codec->capabilities & CODEC_CAP_TRUNCATED) { 186 | icodec->flags |= CODEC_FLAG_TRUNCATED; 187 | } 188 | 189 | // Try to write output headers at the start 190 | if (octx->oformat->flags & AVFMT_GLOBALHEADER) { 191 | ocodec->flags |= CODEC_FLAG_GLOBAL_HEADER; 192 | } 193 | 194 | ocodec->extradata = (uint8_t*)av_mallocz( 195 | icodec->extradata_size + FF_INPUT_BUFFER_PADDING_SIZE); 196 | if (!ocodec->extradata) { 197 | ret = AVERROR(ENOMEM); 198 | goto CLEANUP; 199 | } 200 | memcpy(ocodec->extradata, icodec->extradata, icodec->extradata_size); 201 | ocodec->extradata_size = icodec->extradata_size; 202 | 203 | // Code from avconv, but doesn't seem to work all the time - for now just 204 | // copy the timebase 205 | ocodec->time_base = istream->time_base; 206 | if (av_q2d(icodec->time_base) * icodec->ticks_per_frame > 207 | av_q2d(istream->time_base) && av_q2d(istream->time_base) < 1 / 1000.0) { 208 | ocodec->time_base = icodec->time_base; 209 | ocodec->time_base.num *= icodec->ticks_per_frame; 210 | } else { 211 | ocodec->time_base = istream->time_base; 212 | } 213 | 214 | switch (icodec->codec_type) { 215 | case CODEC_TYPE_VIDEO: 216 | ocodec->pix_fmt = icodec->pix_fmt; 217 | ocodec->width = icodec->width; 218 | ocodec->height = icodec->height; 219 | ocodec->has_b_frames = icodec->has_b_frames; 220 | if (!ocodec->sample_aspect_ratio.num) { 221 | ocodec->sample_aspect_ratio = ostream->sample_aspect_ratio = 222 | istream->sample_aspect_ratio.num ? istream->sample_aspect_ratio : 223 | icodec->sample_aspect_ratio.num ? icodec->sample_aspect_ratio : 224 | (AVRational){0, 1}; 225 | } 226 | if (octx->oformat->flags & AVFMT_GLOBALHEADER) { 227 | ocodec->flags |= CODEC_FLAG_GLOBAL_HEADER; 228 | } 229 | break; 230 | case CODEC_TYPE_AUDIO: 231 | ocodec->channel_layout = icodec->channel_layout; 232 | ocodec->sample_rate = icodec->sample_rate; 233 | ocodec->sample_fmt = icodec->sample_fmt; 234 | ocodec->channels = icodec->channels; 235 | ocodec->frame_size = icodec->frame_size; 236 | ocodec->audio_service_type = icodec->audio_service_type; 237 | if ((icodec->block_align == 1 && icodec->codec_id == CODEC_ID_MP3) || 238 | icodec->codec_id == CODEC_ID_AC3) { 239 | ocodec->block_align = 0; 240 | } else { 241 | ocodec->block_align = icodec->block_align; 242 | } 243 | break; 244 | case CODEC_TYPE_SUBTITLE: 245 | // ? 246 | ocodec->width = icodec->width; 247 | ocodec->height = icodec->height; 248 | break; 249 | default: 250 | break; 251 | } 252 | 253 | // TODO: threading settings 254 | ocodec->thread_count = 2; 255 | 256 | return ostream; 257 | 258 | CLEANUP: 259 | *pret = ret; 260 | // TODO: cleanup ostream? 261 | return NULL; 262 | } 263 | 264 | bool TaskContext::NextPacket(int* pret, Progress* progress, AVPacket& packet) { 265 | TC_LOG_D("TaskContext::NextPacket()\n"); 266 | 267 | AVFormatContext* ictx = this->ictx; 268 | 269 | int ret = 0; 270 | 271 | // Read a few packets to fill our queue before we process 272 | if (!this->doneReading) { 273 | while (this->fifo->GetCount() <= FIFO_MIN_COUNT) { 274 | AVPacket readPacket; 275 | int done = av_read_frame(ictx, &readPacket); 276 | if (done) { 277 | TC_LOG_D("TaskContext::NextPacket(): done/failed to read frame (%d)\n", 278 | ret); 279 | *pret = 0; 280 | this->doneReading = true; 281 | break; 282 | } else { 283 | double timestamp = readPacket.pts * 284 | (double)ictx->streams[readPacket.stream_index]->time_base.num / 285 | (double)ictx->streams[readPacket.stream_index]->time_base.den; 286 | this->fifo->QueuePacket(readPacket.stream_index, readPacket, timestamp); 287 | } 288 | } 289 | } 290 | if (this->doneReading && !this->fifo->GetCount()) { 291 | return true; 292 | } 293 | 294 | // Grab the next packet 295 | if (!this->fifo->DequeuePacket(packet)) { 296 | return true; 297 | } 298 | double timestamp = packet.pts * 299 | (double)ictx->streams[packet.stream_index]->time_base.num / 300 | (double)ictx->streams[packet.stream_index]->time_base.den; 301 | 302 | AVStream* stream = ictx->streams[packet.stream_index]; 303 | 304 | // Ignore if we don't care about this stream 305 | if (stream->discard == AVDISCARD_ALL) { 306 | return false; 307 | } 308 | 309 | ret = av_dup_packet(&packet); 310 | if (ret) { 311 | TC_LOG_D("TaskContext::NextPacket(): failed to duplicate packet (%d)\n", 312 | ret); 313 | av_free_packet(&packet); 314 | *pret = ret; 315 | return true; 316 | } 317 | 318 | AVBitStreamFilterContext* bsfc = this->bitStreamFilter; 319 | while (bsfc) { 320 | AVPacket newPacket = packet; 321 | ret = av_bitstream_filter_filter(bsfc, stream->codec, NULL, 322 | &newPacket.data, &newPacket.size, packet.data, packet.size, 323 | packet.flags & AV_PKT_FLAG_KEY); 324 | if (ret > 0) { 325 | av_free_packet(&packet); 326 | newPacket.destruct = av_destruct_packet; 327 | ret = 0; 328 | } else if (ret < 0) { 329 | // Error! 330 | break; 331 | } 332 | packet = newPacket; 333 | bsfc = bsfc->next; 334 | } 335 | if (ret) { 336 | TC_LOG_D("TaskContext::NextPacket(): failed to filter packet (%d)\n", ret); 337 | *pret = ret; 338 | av_free_packet(&packet); 339 | return true; 340 | } 341 | 342 | // Update progress (only on success) 343 | if (!ret) { 344 | progress->timestamp = timestamp; 345 | } 346 | 347 | if (ret) { 348 | TC_LOG_D("TaskContext::NextPacket() = %d\n", ret); 349 | } 350 | *pret = ret; 351 | return false; 352 | } 353 | 354 | bool TaskContext::WritePacket(int* pret, AVPacket& packet) { 355 | TC_LOG_D("TaskContext::WritePacket()\n"); 356 | 357 | AVFormatContext* ictx = this->ictx; 358 | AVFormatContext* octx = this->octx; 359 | 360 | int ret = 0; 361 | 362 | ret = av_interleaved_write_frame(octx, &packet); 363 | if (ret < 0) { 364 | TC_LOG_D("TaskContext::WritePacket(): could not write frame of " 365 | "stream (%d)\n", ret); 366 | } else if (ret > 0) { 367 | TC_LOG_D("TaskContext::WritePacket(): end of stream requested (%d)\n", ret); 368 | av_free_packet(&packet); 369 | *pret = ret; 370 | return true; 371 | } 372 | 373 | if (ret) { 374 | TC_LOG_D("TaskContext::WritePacket() = %d\n", ret); 375 | } 376 | *pret = ret; 377 | return false; 378 | } 379 | 380 | bool TaskContext::Pump(int* pret, Progress* progress) { 381 | TC_LOG_D("TaskContext::Pump()\n"); 382 | 383 | AVPacket packet; 384 | if (this->NextPacket(pret, progress, packet)) { 385 | TC_LOG_D("TaskContext::Pump() = %d\n", *pret); 386 | return true; 387 | } 388 | 389 | if (this->WritePacket(pret, packet)) { 390 | TC_LOG_D("TaskContext::Pump() = %d\n", *pret); 391 | av_free_packet(&packet); 392 | return true; 393 | } 394 | 395 | av_free_packet(&packet); 396 | 397 | return false; 398 | } 399 | 400 | void TaskContext::End() { 401 | TC_LOG_D("TaskContext::End()\n"); 402 | 403 | AVFormatContext* ictx = this->ictx; 404 | AVFormatContext* octx = this->octx; 405 | 406 | av_write_trailer(octx); 407 | avio_flush(octx->pb); 408 | } 409 | 410 | SingleFileTaskContext::SingleFileTaskContext(io::IOReader* input, 411 | io::IOWriter* output, Profile* profile, TaskOptions* options) : 412 | TaskContext(input, profile, options), 413 | output(output) { 414 | TC_LOG_D("SingleFileTaskContext::SingleFileTaskContext()\n"); 415 | } 416 | 417 | SingleFileTaskContext::~SingleFileTaskContext() { 418 | TC_LOG_D("SingleFileTaskContext::~SingleFileTaskContext()\n"); 419 | 420 | if (this->octx) { 421 | avformat_free_context(this->octx); 422 | } 423 | IOHandle::CloseWhenDone(this->output); 424 | } 425 | 426 | int SingleFileTaskContext::PrepareOutput() { 427 | TC_LOG_D("SingleFileTaskContext::PrepareOutput()\n"); 428 | 429 | int ret = TaskContext::PrepareOutput(); 430 | 431 | AVFormatContext* octx = this->octx; 432 | 433 | // Open output 434 | if (!ret) { 435 | ret = this->output->Open(); 436 | if (ret) { 437 | TC_LOG_D("SingleFileTaskContext::PrepareOutput(): failed open (%d)\n", 438 | ret); 439 | } 440 | } 441 | if (!ret) { 442 | octx->pb = this->output->context; 443 | if (!octx->pb) { 444 | ret = AVERROR_NOENT; 445 | TC_LOG_D("SingleFileTaskContext::PrepareOutput(): no pb (%d)\n", ret); 446 | } 447 | } 448 | 449 | // Write header 450 | if (!ret) { 451 | ret = avformat_write_header(octx, NULL); 452 | if (ret) { 453 | TC_LOG_D("SingleFileTaskContext::PrepareOutput(): failed write_header " 454 | " (%d)\n", ret); 455 | } 456 | } 457 | 458 | TC_LOG_D("SingleFileTaskContext::PrepareOutput() = %d\n", ret); 459 | return ret; 460 | } 461 | 462 | LiveStreamingTaskContext::LiveStreamingTaskContext( 463 | io::IOReader* input, Profile* profile, TaskOptions* options) : 464 | TaskContext(input, profile, options) { 465 | TC_LOG_D("LiveStreamingTaskContext::LiveStreamingTaskContext()\n"); 466 | 467 | this->playlist = new hls::Playlist( 468 | options->liveStreaming->path, options->liveStreaming->name, 469 | options->liveStreaming->segmentDuration, 470 | options->liveStreaming->allowCaching); 471 | } 472 | 473 | LiveStreamingTaskContext::~LiveStreamingTaskContext() { 474 | TC_LOG_D("LiveStreamingTaskContext::~LiveStreamingTaskContext()\n"); 475 | 476 | delete this->playlist; 477 | } 478 | 479 | int LiveStreamingTaskContext::PrepareOutput() { 480 | TC_LOG_D("LiveStreamingTaskContext::PrepareOutput()\n"); 481 | 482 | int ret = 0; 483 | 484 | if (ret) { 485 | TC_LOG_D("LiveStreamingTaskContext::PrepareOutput() = %d\n", ret); 486 | } 487 | return ret; 488 | } 489 | 490 | bool LiveStreamingTaskContext::Pump(int* pret, Progress* progress) { 491 | TC_LOG_D("LiveStreamingTaskContext::Pump()\n"); 492 | 493 | AVPacket packet; 494 | if (this->NextPacket(pret, progress, packet)) { 495 | TC_LOG_D("LiveStreamingTaskContext::Pump() = %d\n", *pret); 496 | return true; 497 | } 498 | 499 | // TODO: switch segment/etc based on packet timeestamp/duration 500 | 501 | if (this->WritePacket(pret, packet)) { 502 | TC_LOG_D("LiveStreamingTaskContext::Pump() = %d\n", *pret); 503 | av_free_packet(&packet); 504 | return true; 505 | } 506 | 507 | av_free_packet(&packet); 508 | 509 | return false; 510 | } 511 | -------------------------------------------------------------------------------- /src/taskcontext.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "utils.h" 4 | #include "packetfifo.h" 5 | #include "profile.h" 6 | #include "taskoptions.h" 7 | #include "hls/playlist.h" 8 | #include "io/io.h" 9 | 10 | #ifndef NODE_TRANSCODING_TASKCONTEXT 11 | #define NODE_TRANSCODING_TASKCONTEXT 12 | 13 | using namespace v8; 14 | 15 | namespace transcoding { 16 | 17 | typedef struct Progress_t { 18 | double timestamp; 19 | double duration; 20 | double timeElapsed; 21 | double timeEstimated; 22 | double timeRemaining; 23 | double timeMultiplier; 24 | } Progress; 25 | 26 | class TaskContext { 27 | public: 28 | TaskContext(io::IOReader* input, Profile* profile, TaskOptions* options); 29 | virtual ~TaskContext(); 30 | 31 | public: 32 | // Occurs exclusively in a worker thread 33 | virtual int PrepareInput(); 34 | virtual int PrepareOutput(); 35 | AVStream* AddOutputStreamCopy(AVFormatContext* octx, AVStream* istream, 36 | int* pret); 37 | bool NextPacket(int* pret, Progress* progress, AVPacket& packet); 38 | bool WritePacket(int* pret, AVPacket& packet); 39 | virtual bool Pump(int* pret, Progress* progress); 40 | void End(); 41 | 42 | public: 43 | io::IOReader* input; 44 | Profile* profile; 45 | TaskOptions* options; 46 | 47 | AVFormatContext* ictx; 48 | AVFormatContext* octx; 49 | 50 | AVBitStreamFilterContext* bitStreamFilter; 51 | 52 | PacketFifo* fifo; 53 | bool doneReading; 54 | }; 55 | 56 | class SingleFileTaskContext : public TaskContext { 57 | public: 58 | SingleFileTaskContext(io::IOReader* input, io::IOWriter* output, 59 | Profile* profile, TaskOptions* options); 60 | virtual ~SingleFileTaskContext(); 61 | 62 | virtual int PrepareOutput(); 63 | 64 | protected: 65 | io::IOWriter* output; 66 | }; 67 | 68 | class LiveStreamingTaskContext : public TaskContext { 69 | public: 70 | LiveStreamingTaskContext(io::IOReader* input, Profile* profile, 71 | TaskOptions* options); 72 | virtual ~LiveStreamingTaskContext(); 73 | 74 | virtual int PrepareOutput(); 75 | virtual bool Pump(int* pret, Progress* progress); 76 | 77 | protected: 78 | hls::Playlist* playlist; 79 | 80 | int segmentId; 81 | io::IOWriter* segmentOutput; 82 | }; 83 | 84 | }; // transcoding 85 | 86 | #endif // NODE_TRANSCODING_TASKCONTEXT 87 | -------------------------------------------------------------------------------- /src/taskoptions.cpp: -------------------------------------------------------------------------------- 1 | #include "taskoptions.h" 2 | 3 | using namespace transcoding; 4 | 5 | LiveStreamingOptions::LiveStreamingOptions(Handle source) { 6 | HandleScope scope; 7 | 8 | this->path = V8GetString(source, "path", "/tmp/"); 9 | this->name = V8GetString(source, "name", "video"); 10 | 11 | this->segmentDuration = V8GetNumber(source, "segmentDuration", 10); 12 | this->allowCaching = V8GetBoolean(source, "allowCaching", true); 13 | } 14 | 15 | LiveStreamingOptions::~LiveStreamingOptions() { 16 | } 17 | 18 | TaskOptions::TaskOptions(Handle source) : 19 | liveStreaming(NULL) { 20 | HandleScope scope; 21 | 22 | Local liveStreaming = 23 | Local::Cast(source->Get(String::New("liveStreaming"))); 24 | if (!liveStreaming.IsEmpty() && liveStreaming->IsObject()) { 25 | this->liveStreaming = new LiveStreamingOptions(liveStreaming); 26 | } 27 | } 28 | 29 | TaskOptions::~TaskOptions() { 30 | if (this->liveStreaming) { 31 | delete this->liveStreaming; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/taskoptions.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "utils.h" 6 | 7 | #ifndef NODE_TRANSCODING_TASKOPTIONS 8 | #define NODE_TRANSCODING_TASKOPTIONS 9 | 10 | using namespace v8; 11 | 12 | namespace transcoding { 13 | 14 | class LiveStreamingOptions { 15 | public: 16 | LiveStreamingOptions(Handle source); 17 | ~LiveStreamingOptions(); 18 | 19 | std::string path; 20 | std::string name; 21 | 22 | double segmentDuration; 23 | bool allowCaching; 24 | }; 25 | 26 | class TaskOptions { 27 | public: 28 | TaskOptions(Handle source); 29 | ~TaskOptions(); 30 | 31 | LiveStreamingOptions* liveStreaming; 32 | }; 33 | 34 | }; // transcoding 35 | 36 | #endif // NODE_TRANSCODING_TASKOPTIONS 37 | -------------------------------------------------------------------------------- /src/utils.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | extern "C" { 7 | #include "libavformat/avformat.h" 8 | #include "libavcodec/avcodec.h" 9 | } 10 | 11 | #ifndef NODE_TRANSCODING_UTILS 12 | #define NODE_TRANSCODING_UTILS 13 | 14 | namespace transcoding { 15 | 16 | // Set to 1 to enable massive debug spew 17 | #define TC_LOG_DEBUG 1 18 | 19 | #if TC_LOG_DEBUG 20 | #define TC_LOG_D(args...) printf(args) 21 | #else 22 | #define TC_LOG_D(msg, ...) 23 | #endif // TC_LOG_DEBUG 24 | 25 | #ifndef countof 26 | #ifdef _countof 27 | #define countof _countof 28 | #else 29 | #define countof(a) (sizeof(a) / sizeof(*(a))) 30 | #endif 31 | #endif 32 | 33 | #define NODE_SET_PROTOTYPE_ACCESSOR(templ, name, callback) \ 34 | do { \ 35 | templ->PrototypeTemplate()->SetAccessor(v8::String::NewSymbol(name), \ 36 | callback); \ 37 | } while (0) 38 | 39 | #define NODE_ON_EVENT(obj, name, inst, callback, target) \ 40 | do { \ 41 | Local __cbt = FunctionTemplate::New(callback, \ 42 | External::New(reinterpret_cast(target))); \ 43 | Local __cb = __cbt->GetFunction(); \ 44 | __cb->SetName(String::New(name)); \ 45 | inst = Persistent::New(__cb); \ 46 | Local __on = Local::Cast(obj->Get(String::New("on"))); \ 47 | __on->Call(obj, 2, (Handle[]){ String::New(name), __cb }); \ 48 | } while(0) 49 | 50 | #define NODE_REMOVE_EVENT(obj, name, inst) \ 51 | do { \ 52 | Local __removeListener = \ 53 | Local::Cast(obj->Get(String::New("removeListener"))); \ 54 | __removeListener->Call(obj, \ 55 | 2, (Handle[]){ String::New(name), inst }); \ 56 | inst.Dispose(); \ 57 | inst.Clear(); \ 58 | } while(0) 59 | 60 | static std::string V8GetString(v8::Handle obj, const char* name, 61 | std::string original) { 62 | v8::HandleScope scope; 63 | v8::Local value = 64 | v8::Local::Cast(obj->Get(v8::String::NewSymbol(name))); 65 | if (value.IsEmpty()) { 66 | return original; 67 | } else { 68 | return *v8::String::Utf8Value(value); 69 | } 70 | } 71 | 72 | static double V8GetNumber(v8::Handle obj, const char* name, 73 | double original) { 74 | v8::HandleScope scope; 75 | v8::Local value = 76 | v8::Local::Cast(obj->Get(v8::String::NewSymbol(name))); 77 | if (value.IsEmpty()) { 78 | return original; 79 | } else { 80 | return value->NumberValue(); 81 | } 82 | } 83 | 84 | static bool V8GetBoolean(v8::Handle obj, const char* name, 85 | bool original) { 86 | v8::HandleScope scope; 87 | v8::Local value = 88 | v8::Local::Cast(obj->Get(v8::String::NewSymbol(name))); 89 | if (value.IsEmpty()) { 90 | return original; 91 | } else { 92 | return value->IsTrue(); 93 | } 94 | } 95 | 96 | }; // transcoding 97 | 98 | #endif // NODE_TRANSCODING_UTILS 99 | -------------------------------------------------------------------------------- /wscript: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import os 5 | 6 | import json 7 | package = json.load(open('package.json')) 8 | NAME = package['name'] 9 | APPNAME = 'node-' + NAME 10 | VERSION = package['version'] 11 | 12 | srcdir = '.' 13 | blddir = 'build' 14 | 15 | def set_options(opt): 16 | opt.tool_options('compiler_cxx') 17 | opt.tool_options('node_addon') 18 | 19 | def configure(conf): 20 | conf.check_tool('compiler_cxx') 21 | conf.check_tool('node_addon') 22 | 23 | conf.env.append_unique('CXXFLAGS', ['-D__STDC_CONSTANT_MACROS']) 24 | 25 | conf.check(header_name='libavformat/avformat.h', mandatory=True) 26 | conf.check(header_name='libavcodec/avcodec.h', mandatory=True) 27 | conf.check(lib='avutil', uselib_store='LIBAVUTIL') 28 | conf.check(lib='avformat', uselib_store='LIBAVFORMAT') 29 | conf.check(lib='avcodec', uselib_store='LIBAVCODEC') 30 | 31 | def build(bld): 32 | t = bld.new_task_gen('cxx', 'shlib', 'node_addon') 33 | t.target = 'node_transcoding' 34 | t.cxxflags = ['-D__STDC_CONSTANT_MACROS'] 35 | t.uselib = ['LIBAVUTIL', 'LIBAVFORMAT', 'LIBAVCODEC'] 36 | t.source = [ 37 | 'src/binding.cpp', 38 | 'src/mediainfo.cpp', 39 | 'src/packetfifo.cpp', 40 | 'src/profile.cpp', 41 | 'src/query.cpp', 42 | 'src/querycontext.cpp', 43 | 'src/task.cpp', 44 | 'src/taskcontext.cpp', 45 | 'src/taskoptions.cpp', 46 | 'src/hls/playlist.cpp', 47 | 'src/io/filereader.cpp', 48 | 'src/io/filewriter.cpp', 49 | 'src/io/io.cpp', 50 | 'src/io/iohandle.cpp', 51 | 'src/io/nullwriter.cpp', 52 | 'src/io/streamreader.cpp', 53 | 'src/io/streamwriter.cpp', 54 | ] 55 | --------------------------------------------------------------------------------