0

Raspberry Pi – Web Controlled Media Player

Having a spare TV and a hard drive full of videos, I wanted to make it so I could easily watch videos from a hard drive on the TV and control playback and video choice entirely from my phone, this is what I set out to achieve with this project.

The entire project can be found on github here: https://github.com/comeradealexi/RPIMediaController

Physical Prerequisites:

  • Raspberry Pi 2 (or newer, I’m unsure of previous Pi’s media playback)
  • LAN connection for Pi (I’m using the official Raspberry Pi WiFi Dongle)
  • Hard Drive/USB containing videos (preferably organised into folders)
  • Any web browser to control playback.

Software Prerequisites:

A general overview of how all components fit together:

JavaScript:

Having never written a line of JavaScript in my life, I kicked off the project by creating a checkbox system. Where it behaves as you’d expect, parents of the selected checkboxes are marked as ‘indeterminate’. I found some interesting examples which shows me how to specifically check & uncheck but I rewrote it to fit my needs. I could probably be more efficient as it loops over all checkboxes multiple times each time one is checked/unchecked but it works.

function UpdateMarkings(event) {
    console.log(event);

    var ChangeType = event.checked;
    var listOfChildren = event.parentElement.parentElement.getElementsByClassName("checkboxClass");
    for (var i = 0; i < listOfChildren.length; i++) {
        listOfChildren[i].checked = ChangeType;
    }

    //Update Indeterminates

    var allCheckboxClasses = document.getElementsByClassName("checkboxClass");
    for (var i = allCheckboxClasses.length - 1; i >= 0; i--) {
        var allChildrenTypes = allCheckboxClasses[i].parentElement.parentElement.getElementsByClassName("checkboxClass");
        var checkCount = 0;
        for (var j = 1; j < allChildrenTypes.length; j++) {
            if (allChildrenTypes[j].checked) checkCount++;
        }

        if (allChildrenTypes.length > 1 && (checkCount + 1) == allChildrenTypes.length) {
            allCheckboxClasses[i].checked = true;
            allCheckboxClasses[i].indeterminate = false;
        } else
        if (checkCount > 0) {
            allCheckboxClasses[i].checked = false;
            allCheckboxClasses[i].indeterminate = true;
        } else
            allCheckboxClasses[i].indeterminate = false;
    }

}

All the checkbox html is generated by the C++ program which recursively searches a specified folder for videos with certain file extensions (avi, mp4, m4a and mkv). It then builds up the list, assigning an ID to every checkbox which doesn’t have any children (i.e is a video file).

When ‘Play Selected’ is called. A JavsScript function is called to gather a list of ID’s of all the ticked non-childen checkboxes:

function GatherAllTicked()
{
	var checkedList = [];
	var rejectList = [];
	var allCheckboxClasses = document.getElementsByClassName("checkboxClass");
    for(var i = 0; i < allCheckboxClasses.length; i++)
    {
		if (allCheckboxClasses[i].checked == true && include(rejectList, allCheckboxClasses[i].id) == false)
        {
			var allChildrenTypes = allCheckboxClasses[i].parentElement.parentElement.getElementsByClassName("checkboxClass");
			var AllChildrenChecked = true;
			for(var j = 1; j < allChildrenTypes.length; j++)
			{
				if (allChildrenTypes[j].checked == false)
					AllChildrenChecked = false;
			}
			if (AllChildrenChecked == true)
			{
				//checkedList.push(allCheckboxClasses[i].id);
				for(var j = 1; j < allChildrenTypes.length; j++) 
				{
					if (allChildrenTypes[j].name == "file")
						checkedList.push(allChildrenTypes[j].id); 
					else 
						rejectList.push(allChildrenTypes[j].id);
				}
			}
		}
	}
	return checkedList;
}

JQuery & Ajax is used to call a php script and POST the list.

The other bit of JavaScript in the project is to call a PHP script periodically which opens a file, containing the name of the current playing video. This is then shown on the web page.

PHP:

The PHP in this project is fairly simple, all the scripts simple open a file and either read/write to them. In some cases it’s just a normal file and others it’s a FIFO file so it can directly communicate with the running C++ program.

The PHP script below receives a ‘cmd’ url parameter containing a single character which is sent to the C++ program to Pause/Play/Change Volume/Stop etc.

Currently the PHP scripts just have hard coded paths, a more elegant, dynamic solution would be nice.

<?php
$paramAdded = $_GET['cmd'];

$stdInFilePath = '/home/pi/projects/RaspberryPiPlayer/bin/ARM/Debug/thefifofile'; //'/proc/' . $pidTxt . '/fd/1';

file_put_contents('cmd.txt', $paramAdded);

$pStdInFile = fopen($stdInFilePath, 'w');

if ($pStdInFile && $paramAdded)
{
	fwrite($pStdInFile, $paramAdded);
	fwrite($outputFile, "Writing to the std in\n"); 
	fclose($pStdInFile);
}
?>

C++ Program:

The majority of coding for this project was in the C++ program. Which fills key roles:

  • Listens for input via FIFO file from PHP scripts.
  • Makes system calls to omxplayer, via a pipe to write keyboard input directly to it.
  • Generate the html list of checkboxes.

It’s all contained in a single .cpp file, you can see it on the github repository linked at the top of this post.

Below is the recursive function to generate the checkbox html. The LookupList is a std::map<int,std::string>. So when we get commands through from the PHP with a list of video ID’s they can quickly be looked up in the map and the full file path of the video obtained.

void RecursivelyBuildListHtml(char* pszPath, FILE* pOutputFile, const char* pPrevName, int iDepth, int& ID, LookupList& lookupList)
{
	char tmpPath[1024];
	DIR* dp = opendir(pszPath);
	dirent* dirp;

	if (dp == nullptr)
		return;

	while ((dirp = readdir(dp)) != NULL) 
	{
		if (dirp->d_type == DT_DIR || dirp->d_type == DT_REG)
		{
			if (dirp->d_type == DT_DIR && ShouldIgnoreDir(dirp->d_name))
				continue;

			if (dirp->d_type == DT_REG && ShouldIgnoreReg(dirp->d_name))
				continue;

			snprintf(tmpPath, sizeof(tmpPath), "%s/%s", pszPath, dirp->d_name);

			const char* displayType = iDepth >= 0 ? "display: none;" : "display: ;";
			const char* szName = dirp->d_type == DT_REG ? "name=\"file\"" : "";

			if (dirp->d_type == DT_REG) 
				lookupList[ID] = tmpPath;

			fprintf(pOutputFile, "<li>" "<img style=\"width:15px; height:15px; background - color: lightblue;\" src=\"down.png\" onclick=\"dropdownfunction(this)\"></img>" "<label><input %s onclick=\"UpdateMarkings(this)\" type=\"checkbox\" id=\"%s\" class=\"checkboxClass\">%s</label>\n", szName, std::to_string(ID++).c_str(), dirp->d_name);
			if (dirp->d_type == DT_DIR)
			{
				fprintf(pOutputFile, "<ul style=\"%s\">\n", displayType);
				RecursivelyBuildListHtml(tmpPath, pOutputFile, dirp->d_name, iDepth + 1, ID, lookupList);
				fprintf(pOutputFile, "</ul>\n");
			}
			fprintf(pOutputFile, "</li>\n");
		}
	}
}

Thoughts about this project:

This is a little project I’ve really enjoyed working on because it has taught me a lot of new things about Linux, the Raspberry Pi and especially JavaScript. Having not written any JavaScript before, I felt this was a really good way to get a good overview of how it works and how it can be used to communicate with a running C++ project. I’ve no doubt what I’ve learnt from this project will be useful again in the future.

The great thing about this project is that I’m genuinely going to use what I’ve created and it will be super handy.

Improvements To Be Made:

  • I’m no graphic designer and the web page I’ve created doesn’t look that appealing.
  • The web page isn’t super mobile friendly.
  • The C++ program contains the header and footer of the html web page, ideally this would be in its own Index.html page, then there would be a php script to fetch the cached checkbox lists from a file.
  • Currently the program will play the selection at random, it would be good to have a choice to play the selection in alphabetical order.
  • If you’ve got someone evil on your WiFi they’ll be able to fully control it as much as anyone else, so beware!