I couldnt get the EXT-FreeboxTV module to work on my magic mirror so i gave up, and used chatgpt to help me write a module.
Its called MMM-MPlayer, and using MPlayer it will open either 1 or 2 Mplayer windows and play whatever files or rtsp streams you like. If you define multiple it will cycle through them, determined by the streamInterval. I am using it to stream 3 Unifi Cameras so that i can keep an eye on the front yard in one window, and an eye on my kids via the cameras we use as nanny cams while they nap/sleep, cycling through those two rooms every 30 seconds.
Reasons for using MPlayer over something else is that the version of raspbian that I am running on my pi3 has a broken version of VLC, which doesnt respond to command line position arguments, and also cant run OMXplayer. Its the most recent distro, i dont remember the name for it. mplayer works well, and plays on my pi3 just fine, along with all the other stuff i have running.
Obviously your use case could be for anything, cameras around your house, livestreams you like to watch. Essentially anything that mplayer can play. The module natively responds to the MMM-Pir notification so that it kills and restarts the streams when the PIR module tells the screen to turn off and on, so that the screen acts as it should.
I don’t feel like actually uploading it to github, but here is the source code, and an example config.
--------------- CONFIG.js
{
module: 'MMM-MPlayer', // Update the module name here
position: 'top_left', // Magic Mirror position
config: {
useTwoWindows: true, // Use two windows
layout: 'row', // Can be 'row' or 'column'
windowSize: { width: 525, height: 295 }, // Window size for both windows
windowPosition: { x: 12, y: 575 }, // Position of the first window (window1) [window2 is either 5px below or to the right of this window, depending on layout]
streamInterval: 30000,
streams: {
window1: [
'somthing_else.mp4',
'something.mp4'
],
window2: [
'rtsp://foo',
'rtsp://bar',
]
}
}
},
---------------MMM-MPlayer.js
/* MagicMirror Module: MMM-MPlayer.js
* This script communicates with the backend (node_helper.js) to control mplayer window streaming
* It starts the stream cycle process only after the DOM is fully loaded.
*/
Module.register('MMM-MPlayer', {
// Define the module's defaults
defaults: {
useTwoWindows: true,
layout: 'column', // Can be 'row' or 'column'
windowSize: { width: 640, height: 480 },
windowPosition: { x: 0, y: 0 }, // Position of the first window
streamInterval:30000,
streams: {
window1: [
'http://stream1.example.com/video1',
'http://stream2.example.com/video1'
],
window2: [
'http://stream1.example.com/video2',
'http://stream2.example.com/video2'
]
}
},
// Start the module
start: function() {
console.log('MMM-MPlayer module starting...');
// Send the configuration to the backend
this.sendSocketNotification('SET_CONFIG', this.config);
// Notify the backend to start the stream cycle (backend will handle stream cycling)
//this.sendSocketNotification('START_STREAM_CYCLE');
},
// Define socket notification handlers
socketNotificationReceived: function(notification, payload) {
switch(notification)
{
case 'STREAM_CYCLE_STARTED':
console.log('Stream cycle process started.');
break;
}
},
// This function listens to the DOM_CREATED event and starts the stream cycle process
notificationReceived: function(notification, payload, sender) {
switch(notification) {
case 'DOM_OBJECTS_CREATED':
console.log('DOM created. Starting the stream cycle process...');
// Send the notification to the backend to initiate the stream cycle
this.sendSocketNotification('START_STREAM_CYCLE');
break;
case 'MMM_PIR-SCREEN_POWERSTATUS':
console.log(`Received PIR Screen Show Notification ${payload}`);
if (payload == true) {
this.sendSocketNotification('START_STREAM_CYCLE');
}
else {
this.sendSocketNotification('STOP_STREAM_CYCLE');
}
break;
}
},
getDom: function() {
var wrapper = document.createElement("div");
wrapper.innerHTML = '';
return wrapper;
}
});
------------------ NODE HELPER
const NodeHelper = require('node_helper');
const { spawn } = require('child_process');
const { os } = require('os');
const Log = require('logger'); // Import the Log module from MagicMirror
module.exports = NodeHelper.create({
start: function() {
Log.info('Starting MMM-MPlayer module...');
this.streams = {};
this.currentStreamIndex = { window1: -1, window2: -1 };
this.mplayerProcesses = { window1: null, window2: null }; // Track mplayer processes for each window
this.streamInterval = 30000;
this.streamSwitcher = null;
},
// Handle socket notifications from the frontend
socketNotificationReceived: function(notification, payload) {
switch(notification) {
case 'SET_CONFIG':
Log.info('Received configuration for MMM-MPlayer module');
// Save the configuration
this.config = payload;
// Adjust layout and start the stream cycle
this.adjustLayout();
break;
case 'START_STREAM_CYCLE':
Log.info('Stream cycle process started.');
this.cycleStreams(); // Start the stream cycle after receiving the notification
break;
case 'STOP_STREAM_CYCLE':
Log.info('Stream cycle process stopped.');
this.stopStreams();
}
},
// Start or refresh the streams
cycleStreams: function() {
//fire up the streams immediately
this.switchStream('window1');
this.switchStream('window2');
if (this.streamSwitcher == null) {
this.streamSwitcher = setInterval(() => {
this.switchStream('window1');
this.switchStream('window2');
}, this.config.streamInterval); // cycle based on the config
this.sendSocketNotification('STREAM_CYCLE_STARTED');
}
},
stopStreams: function() {
if (this.streamSwitcher != null) {
clearInterval(this.streamSwitcher);
this.killMPlayer('window1');
this.killMPlayer('window2');
this.streamSwitcher = null;
this.currentStreamIndex = { window1: -1, window2: -1 };
}
},
// Switch the stream for the given window
switchStream: function(window) {
const windowStreams = this.config.streams[window];
Log.info(`Switching stream for ${window}`);
const currentIndex = this.currentStreamIndex[window];
const nextIndex = (currentIndex + 1) % windowStreams.length;
// Update stream index
this.currentStreamIndex[window] = nextIndex;
if (currentIndex != nextIndex) {
// Kill the old mplayer process for the window using SIGTERM
this.killMPlayer(window);
// Launch new mplayer process for the window
this.launchMPlayer(windowStreams[nextIndex], window);
}
},
// Kill any existing mplayer process for a window using SIGTERM
killMPlayer: function(window) {
const mplayerProcess = this.mplayerProcesses[window];
if (mplayerProcess) {
Log.info(`Killing mplayer process for ${window}...${mplayerProcess.pid}`);
const killer = spawn(`kill`, [`${mplayerProcess.pid}`]);
// Handle standard output and error
killer.stdout.on('data', (data) => {
Log.debug(`killer [${window}] stdout: ${data}`);
});
killer.stderr.on('data', (data) => {
Log.error(`killer [${window}] stderr: ${data}`);
});
killer.on('close', (code) => {
Log.info(`killer process for ${window} exited with code ${code}`);
});
}
},
// Launch a new mplayer process for the window using spawn
launchMPlayer: function(stream, window) {
const size = this.config.windowSize;
const position = this.config[`${window}Position`] || this.config.windowPosition; // Use specific or general window position
// Spawn a new mplayer process
const env = { ...process.env, DISPLAY: ':0' };
const mplayerProcess = spawn(`mplayer`, ['-noborder', '-geometry', `${position.x}:${position.y}`, `-xy`, `${size.width}`, `${size.height}`, `${stream}`], {env: env}); //C,
Log.info(`Launched mplayer process for ${window} with PID ${mplayerProcess.pid}`);
// Track the process for future termination
this.mplayerProcesses[window] = mplayerProcess;
// Handle standard output and error
mplayerProcess.stdout.on('data', (data) => {
Log.debug(`mplayer [${window}] stdout: ${data}`);
});
mplayerProcess.stderr.on('data', (data) => {
//Log.error(`mplayer [${window}] stderr: ${data}`);
});
mplayerProcess.on('close', (code) => {
Log.info(`mplayer process for ${window} exited with code ${code}`);
});
},
// Adjust stream positions and size based on layout
adjustLayout: function() {
const windowPosition = this.config.windowPosition; // General window position for window1
const windowSize = this.config.windowSize;
const layout = this.config.layout;
// Calculate position for second window automatically based on layout
if (layout === 'column') {
// If layout is column, position window 2 below window 1
this.config.window2Position = {
x: windowPosition.x, // Same x position
y: windowPosition.y + windowSize.height + 5 // y position of window2 is below window1
};
} else if (layout === 'row') {
// If layout is row, position window 2 to the right of window 1
this.config.window2Position = {
x: windowPosition.x + windowSize.width + 5, // x position of window2 is to the right of window1
y: windowPosition.y // Same y position
};
}
}
});