├── lib ├── watchdog.js ├── loop_helper.js └── main.js ├── package.json └── README.md /lib/watchdog.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | 3 | /* 4 | * Check that only one process of omxplayer is running 5 | * Fixes problems with omxplayer not exiting. 6 | */ 7 | var onlyOneProcess = function () { 8 | exec('ps xa | grep "[o]mxplayer.bin" | wc -l', function (error, stdout, stderr) { 9 | var processCount = parseInt(stdout); 10 | if (processCount > 1) { 11 | exec('killall -9 omxplayer.bin'); 12 | } 13 | }); 14 | }; 15 | 16 | module.exports.onlyOneProcess = onlyOneProcess; -------------------------------------------------------------------------------- /lib/loop_helper.js: -------------------------------------------------------------------------------- 1 | var spawn = require('child_process').spawn; 2 | 3 | var LoopHelper = function(files, loop) { 4 | var that = {}; 5 | var current = 0; 6 | var getNext = function() { 7 | current += 1; 8 | if (current === files.length) { 9 | if (loop) { 10 | current = 0; 11 | } else { 12 | return null; 13 | } 14 | } 15 | return files[current]; 16 | }; 17 | that.getNext = getNext; 18 | return that; 19 | }; 20 | 21 | module.exports.LoopHelper = LoopHelper; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "omxdirector", 3 | "version": "0.1.2", 4 | "description": "Provide a nodejs interface for omxplayer (Raspberry Pi player). Support multiple file and loop.", 5 | "main": "lib/main.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/alepez/omxdirector.git" 9 | }, 10 | "contributors":[ 11 | {"name":"Jesse Garrison", 12 | "email":"jesse@takethefort.com" 13 | }], 14 | "keywords": [ 15 | "Raspberry", 16 | "pi", 17 | "Raspberry Pi", 18 | "rpi", 19 | "omxplayer", 20 | "omx", 21 | "loop", 22 | "video", 23 | "player" 24 | ], 25 | "author": "Alessandro Pezzato ", 26 | "license": "BSD" 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # omxdirector 2 | 3 | Nodejs module providing a simple interface to omxplayer. 4 | 5 | Supports multiple files playing and loops. It's capable of seamless 6 | loops if `omxplayer` supports it natively. When `omxplayer` doesn't 7 | support loops, this module handles loops respawning omxplayer 8 | process. 9 | 10 | This version supports playing remote (HTTP) files and allow users to seek (-/+ 30 seconds). 11 | 12 | ## Usage 13 | 14 | ### Basic usage 15 | 16 | var omx = require('omxdirector'); 17 | omx.play('video.avi'); 18 | omx.play('http://www.server.com/video.mp4'); 19 | 20 | ### Multiple files 21 | 22 | omx.play(['video.mp4', 'anothervideo.mp4', 'foo.mp4'], {loop: true}); 23 | 24 | **WARNING:** at this time, multiple files playing is not supported by *official* **omxplayer**. 25 | If using with a fork version, you must enable native loop support (see below). 26 | 27 | ### Options 28 | 29 | - **audioOutput** `"local"` or `"hdmi"` as `-o` omxplayer argument. If not specified or `"default"` is system default. 30 | - **loop** `true` to enable `--loop` omxplayer argument. Default is `false`. 31 | - **osd** 'false' to enable '--no-osd' omxplayer argument. Default is 'true.' 32 | 33 | **WARNING:** at this time, loop is not supported by *official* **omxplayer**. 34 | If using with a fork version, you must enable native loop support. 35 | 36 | #### Example 37 | 38 | omx.play('video.mp4', {loop: true}); // enables loop 39 | omx.play('video.mp4', {audioOutput: 'local'}); // analog audio output 40 | 41 | ### Native loop support 42 | 43 | If you have a versione of `omxplayer` supporting native loop with `--loop` flag, 44 | you can enable it by calling: 45 | 46 | var omx = require('omxdirector').enableNativeLoop(); 47 | 48 | ### Loop fallback 49 | 50 | If using with standard omxplayer, a fallback is provided: once a video is finished, 51 | another process of omxplayer is launched. It support multiple files and infinite loop. 52 | Although this works fine, native support is better because there's no gap between video. 53 | 54 | ### Status 55 | 56 | omx.getStatus() 57 | 58 | Return an object with current status: 59 | 60 | If process is not running: 61 | 62 | { loaded: false } 63 | 64 | If process is running: 65 | 66 | { 67 | loaded: true, 68 | videos: , // videos array passed to play(videos, options) 69 | settings: , // default settings or options object passed to play(videos, options) 70 | playing: // true if not paused, false if paused 71 | } 72 | 73 | ### Video directory 74 | 75 | omx.setVideoDir(path); 76 | 77 | Set where to look for videos. Useful when all videos are in the same directory. 78 | 79 | Instead of this: 80 | 81 | omx.play(['/home/pi/videos/foo.mp4', '/home/pi/videos/bar.mp4', '/home/pi/videos/asdasd.mp4']); 82 | 83 | It's possible to use this shortcut: 84 | 85 | omx.setVideoDir('/home/pi/videos/'); 86 | omx.play(['foo.mp4', 'bar.mp4', 'asdasd.mp4']); 87 | 88 | ### Video suffix 89 | 90 | omx.setVideoSuffix(suffix); 91 | 92 | Set a suffix for videos. Useful when all videos share the same format. 93 | 94 | Instead of this: 95 | 96 | omx.play(['foo.mp4', 'bar.mp4', 'asdasd.mp4']); 97 | 98 | It's possible to use this shortcut: 99 | 100 | omx.setVideoSuffix('.mp4'); 101 | omx.play(['foo', 'bar', 'asdasd']); 102 | 103 | ### Other methods 104 | 105 | omx.pause(); // pause the video 106 | omx.play(); // resume video playing 107 | omx.stop(); // stop video playing and terminate omxplayer process 108 | omx.isLoaded(); // return true if omxprocess is running 109 | omx.isPlaying(); // return true if omxprocess is running and video is not paused 110 | omx.volup(); // Increases the volume one notch 111 | omx.voldown(); // Decreases the volume one notch 112 | omx.backwards(); // Seek by -30 seconds 113 | omx.forwards(); // Seek by +30 seconds 114 | 115 | ### Events 116 | 117 | omx.on('load', function(files, options){}); // video successfully loaded (omxprocess starts) 118 | omx.on('play', function(){}); // when successfully started or resumed from pause 119 | omx.on('pause', function(){}); // when successfully paused 120 | omx.on('stop', function(){}); // when successfully stopped (omxplayer process ends) 121 | 122 | ## TODO 123 | 124 | - Emit event when each video start, stop etc... 125 | - Implement forward/backward. 126 | - Enable a fallback for loop and multiple files when native support is disabled. 127 | 128 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | var spawn = require('child_process').spawn; 2 | var util = require('util'); 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var EventEmitter = require('events').EventEmitter; 6 | var LoopHelper = require('./loop_helper.js').LoopHelper; 7 | var exec = require('child_process').exec; 8 | var onlyOneProcess = require('./watchdog.js').onlyOneProcess; 9 | 10 | /* keep compatibility with older versions of nodejs */ 11 | if (fs.existsSync) { 12 | path.existsSync = fs.existsSync; 13 | } else { 14 | fs.existsSync = path.existsSync; 15 | } 16 | 17 | var omxdirector = function () { 18 | 19 | var that = Object.create(EventEmitter.prototype); 20 | 21 | var nativeLoop = false; 22 | var handleExitHang = false; 23 | 24 | var videoDir = './'; 25 | var videoSuffix = ''; 26 | 27 | var currentVideos = []; 28 | var currentSettings = {}; 29 | 30 | var loopHelper = null; 31 | 32 | var commands = { 33 | 'pause': 'p', 34 | 'quit': 'q', 35 | 'volup': '+', 36 | 'voldown': '-', 37 | 'backwards':'\x1b\x5b\x44', 38 | 'forwards':'\x1b\x5b\x43', 39 | }; 40 | 41 | var omxProcess = null; 42 | var paused = false; 43 | 44 | var sendAction = function (action) { 45 | if (commands[action] && omxProcess) { 46 | try { 47 | omxProcess.stdin.write(commands[action], function (err) { 48 | console.log(err); 49 | }); 50 | } catch (err) { 51 | console.log(err); 52 | } 53 | } 54 | }; 55 | 56 | var resolveFilePaths = function (videos) { 57 | /* reset currentFiles, it will contain only valid videos */ 58 | currentVideos = []; 59 | var ret = []; 60 | var re = /(rtsp|rtmp|udp|http|https)/; 61 | videos.forEach(function (video) { 62 | if(re.exec(video) != null) { 63 | ret.push(video); 64 | currentVideos.push(video); 65 | 66 | return; 67 | } 68 | else 69 | { 70 | var realPath = path.resolve(videoDir, video + videoSuffix); 71 | if (fs.existsSync(realPath)) { 72 | ret.push(realPath); 73 | currentVideos.push(video); 74 | } else { 75 | that.emit('error', new Error('File does not exist: ' + realPath)); 76 | } 77 | } 78 | }); 79 | return ret; 80 | }; 81 | 82 | /* 83 | * Get video length and check, after its duration, if 84 | * process is hanging. Correct a bug of omxplayer 85 | * https://github.com/popcornmix/omxplayer/issues/124 86 | * https://github.com/popcornmix/omxplayer/issues/12 87 | */ 88 | var startWatchdog = function (filename, pid) { 89 | if (!handleExitHang) { 90 | return; 91 | } 92 | getVideoLength(filename, function (durationMs) { 93 | setTimeout(function () { 94 | if (pid === omxProcess.pid) { 95 | omxProcess.kill('SIGKILL'); 96 | } 97 | }, durationMs + 10000); 98 | }); 99 | }; 100 | 101 | var open = function (videos, options) { 102 | var settings = options || {}; 103 | currentSettings = settings; 104 | var cmd = 'omxplayer'; 105 | 106 | var respawn = null; 107 | 108 | var args = []; 109 | if (settings.audioOutput && settings.audioOutput !== 'default') { 110 | args.push('-o'); 111 | args.push(settings.audioOutput); 112 | } 113 | 114 | if (settings.loop === true) { 115 | if (nativeLoop) { 116 | args.push('--loop'); 117 | } 118 | } 119 | 120 | if (settings.osd == false){ 121 | args.push('--no-osd') 122 | } 123 | 124 | if (settings.args) { 125 | args = args.concat(settings.args); 126 | } 127 | 128 | if (typeof videos === 'string') { 129 | videos = [videos]; 130 | } 131 | 132 | var realfiles = resolveFilePaths(videos); 133 | 134 | if (nativeLoop) { 135 | /* all files to omxplayer parameters */ 136 | args.push.apply(args, realfiles); 137 | } else { 138 | /* 139 | * only first file to omxplayer parameter. following files are handled by 140 | * loopHelper 141 | */ 142 | args.push(realfiles[0]); 143 | } 144 | 145 | if (!nativeLoop && ((realfiles.length > 1) || (settings.loop))) { 146 | /* no native loop support, enable helper */ 147 | loopHelper = LoopHelper(realfiles, settings.loop); 148 | 149 | respawn = function () { 150 | if (!loopHelper) { 151 | /* respawn ignored, stop requested */ 152 | that.emit('stop'); 153 | return; 154 | } 155 | /* change file */ 156 | var nextFile = loopHelper.getNext(); 157 | if (!nextFile) { 158 | /* respawn ignored, loop ended */ 159 | loopHelper = null; 160 | that.emit('stop'); 161 | } else { 162 | /* Issue #16: Add event emitter when next file plays */ 163 | that.emit('next', nextFile); 164 | /* respawn */ 165 | args[args.length - 1] = nextFile; 166 | omxProcess = spawn(cmd, args, { 167 | stdio: ['pipe', null, null] 168 | }); 169 | omxProcess.once('exit', respawn); 170 | /* check if omxplayer hangs when video should be finished */ 171 | startWatchdog(nextFile, omxProcess.pid); 172 | } 173 | }; 174 | omxProcess = spawn(cmd, args, { 175 | stdio: ['pipe', null, null] 176 | }); 177 | omxProcess.once('exit', respawn); 178 | } else { 179 | /* native loop support enabled or not requested */ 180 | 181 | omxProcess = spawn(cmd, args, { 182 | stdio: ['pipe', null, null] 183 | }); 184 | 185 | omxProcess.once('exit', function (code, signal) { 186 | omxProcess = null; 187 | that.emit('stop'); 188 | }); 189 | } 190 | 191 | that.emit('load', videos, options); 192 | }; 193 | 194 | var play = function (videos, options) { 195 | if (omxProcess) { 196 | if (!paused) { 197 | return false; 198 | } 199 | sendAction('pause'); 200 | paused = false; 201 | that.emit('play'); 202 | return true; 203 | } 204 | if (!videos) { 205 | throw new TypeError("No files specified"); 206 | } 207 | if (typeof videos !== 'string' && !util.isArray(videos)) { 208 | throw new TypeError("Incorrect value for videos: " + videos); 209 | } 210 | open(videos, options); 211 | that.emit('play'); 212 | return true; 213 | }; 214 | 215 | var pause = function () { 216 | if (paused) { 217 | return false; 218 | } 219 | sendAction('pause'); 220 | paused = true; 221 | that.emit('pause'); 222 | return true; 223 | }; 224 | 225 | var volup = function () { 226 | sendAction('volup'); 227 | that.emit('volup'); 228 | return true; 229 | }; 230 | 231 | var voldown = function () { 232 | sendAction('voldown'); 233 | that.emit('voldown'); 234 | return true; 235 | }; 236 | 237 | var backwards = function () { 238 | sendAction('backwards'); 239 | that.emit('backwards'); 240 | return true; 241 | }; 242 | 243 | var forwards = function () { 244 | sendAction('forwards'); 245 | that.emit('forwards'); 246 | return true; 247 | }; 248 | 249 | var handleQuitTimeout = function (oldOmxProcess, timeout) { 250 | var timeoutHandle = setTimeout(function () { 251 | console.log('omxplayer still running. kill forced'); 252 | oldOmxProcess.kill('SIGTERM'); 253 | }, timeout); 254 | oldOmxProcess.once('exit', function () { 255 | clearTimeout(timeoutHandle); 256 | }); 257 | }; 258 | 259 | var stop = function () { 260 | if (!omxProcess) { 261 | /* ignore, no omxProcess to stop */ 262 | return false; 263 | } 264 | paused = false; 265 | loopHelper = null; 266 | sendAction('quit'); 267 | handleQuitTimeout(omxProcess, 250); 268 | omxProcess = null; 269 | return true; 270 | }; 271 | 272 | var isPlaying = function () { 273 | return omxProcess && !paused; 274 | }; 275 | 276 | var isLoaded = function () { 277 | return omxProcess; 278 | }; 279 | 280 | var getStatus = function () { 281 | if (isLoaded()) { 282 | return { 283 | videos: currentVideos, 284 | settings: currentSettings, 285 | playing: isPlaying(), 286 | loaded: true 287 | }; 288 | } 289 | return { 290 | loaded: false 291 | }; 292 | }; 293 | 294 | var setVideoDir = function (dir) { 295 | videoDir = dir; 296 | }; 297 | 298 | var setVideoSuffix = function (suffix) { 299 | videoSuffix = suffix; 300 | }; 301 | 302 | var enableNativeLoop = function () { 303 | nativeLoop = true; 304 | return that; 305 | }; 306 | 307 | setInterval(onlyOneProcess, 5000); 308 | 309 | that.play = play; 310 | that.pause = pause; 311 | that.stop = stop; 312 | that.backwards = backwards; 313 | that.forwards = forwards; 314 | that.volup = volup; 315 | that.voldown = voldown; 316 | that.isPlaying = isPlaying; 317 | that.isLoaded = isLoaded; 318 | that.getStatus = getStatus; 319 | that.setVideoDir = setVideoDir; 320 | that.setVideoSuffix = setVideoSuffix; 321 | that.enableNativeLoop = enableNativeLoop; 322 | 323 | return that; 324 | }; 325 | 326 | module.exports = omxdirector(); 327 | --------------------------------------------------------------------------------