Stream Video with CakePHP pt.1 | CakePHP blog
Have you ever wanted to build your own streaming platform that rivals Netflix (Hulu, Peacock, Disney+, Amazon Prime, Apple Tv, HBO Max, etc.)? Well, if I knew how to do that I probably would be too rich to blog, but I can help you create a basic video streaming service backed by CakePHP!
To accomplish this all we’ll need is CakePHP, some videos, and a place to host them!
Setting up CakePHP
If you’ve never used CakePHP I highly recommend checking out this guide to help you get started.
Let’s start by creating a new skeleton
php composer.phar create-project --prefer-dist cakephp/app:4.* app
If you’re using WSL like me, then use:
php composer.phar create-project --prefer-dist "cakephp/app:4.*" app
Once Composer finishes creating your app check that it works by running
//Run this inside of your project folder bin/cake server
Creating our Database Structure
We’ll start the design of the app at the M of MVC. For simplification’s sake, we won’t worry about user profiles. We are just creating a library of streaming videos!
Since we’re just fetching a list of videos we’ll only need one table
create database app use app CREATE TABLE videos (
id INT(10) AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
thumbnail_location VARCHAR(255) NOT NULL,
file_name VARCHAR(255) NOT NULL );
The longest movie title I could find is 168 characters, so a VARCHAR(255)
works fine in this case but if you're a stickler for edge cases you could convert it to a TEXT
Be sure to update your config/app_local.php with your database username and password
'Datasources' => [
'default' => [ ...
'username' => 'app_u',
'password' => '[email protected]$$w0rd',
'database' => 'app',
...
],
];
And now you should be getting all green lights from the CakePHP start page! 🎉
Generate Code
Instead of manually creating all the controller and model files we’ll take advantage of Cake’s built-in code generator!
bin/cake bake all Videos #or php bin/cake.php bake all Videos
After this, you should have a full MVC skeleton for your Netflix-killer 😎
Now if you start your server again ( bin/cake server
) and go to http://localhost:8765/videos you should see this:
Yours will say File Name not File Location
In part 2 we’ll use a plugin to upload videos, but for now, you can check out my picks for the top 5 CakePHP plugins
Updating The Controller
Now that we have the app running let’s go ahead and add the streaming logic to our Videos controller. We’ll be using a slightly modified version of this simple video streaming script. It only streams mp4 and has a fix-width byte size, but it will work for demo purposes.
The way the script works is pretty simple. It sets the file headers so that our video player knows it’s getting a video and sends chunks, whose size is determined by the $buffer
constant, to the player. This script would work with sending any file from client to server given the right headers.
Now let’s add the code to our app/src/Controller/VideosController.php
class VideosController extends AppController
{
private $path = "";
private $stream = "";
private $buffer = 102400;
private $start = -1;
private $end = -1;
private $size = 0;
//...
private function setPath($filePath)
{
$this->path = $filePath;
}
/**
* Open stream
*/
private function open()
{
if (!($this->stream = fopen($this->path, 'rb'))) {
die('Could not open stream for reading');
}
}
/**
* Set proper header to serve the video content
*/
private function setHeader()
{
ob_get_clean();
header("Content-Type: video/mp4");
header("Cache-Control: max-age=2592000, public");
header("Expires: ".gmdate('D, d M Y H:i:s', time()+2592000) . ' GMT');
header("Last-Modified: ".gmdate('D, d M Y H:i:s', @filemtime($this->path)) . ' GMT' );
$this->start = 0;
$this->size = filesize($this->path);
$this->end = $this->size - 1;
header("Accept-Ranges: 0-".$this->end);
if (isset($_SERVER['HTTP_RANGE'])) {
$c_start = $this->start;
$c_end = $this->end;
list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
if (strpos($range, ',') !== false) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $this->start-$this->end/$this->size");
exit;
}
if ($range == '-') {
$c_start = $this->size - substr($range, 1);
}else{
$range = explode('-', $range);
$c_start = $range[0];
$c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $c_end;
}
$c_end = ($c_end > $this->end) ? $this->end : $c_end;
if ($c_start > $c_end || $c_start > $this->size - 1 || $c_end >= $this->size) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $this->start-$this->end/$this->size");
exit;
}
$this->start = $c_start;
$this->end = $c_end;
$length = $this->end - $this->start + 1;
fseek($this->stream, $this->start);
header('HTTP/1.1 206 Partial Content');
header("Content-Length: ".$length);
header("Content-Range: bytes $this->start-$this->end/".$this->size);
}
else
{
header("Content-Length: ".$this->size);
}
}
/**
* close curretly opened stream
*/
private function end()
{
fclose($this->stream);
exit;
}
/**
* perform the streaming of calculated range
*/
private function stream()
{
$i = $this->start;
set_time_limit(0);
while(!feof($this->stream) && $i <= $this->end) {
$bytesToRead = $this->buffer;
if(($i+$bytesToRead) > $this->end) {
$bytesToRead = $this->end - $i + 1;
}
$data = @stream_get_contents($this->stream, $bytesToRead, intval($i));
echo $data;
flush();
$i += $bytesToRead;
}
}
/**
* Start streaming video content
*/
public function start($path)
{
$this->setPath(WWW_ROOT . 'img/' . $path)
$this->open();
$this->setHeader();
$this->stream();
$this->end();
}
}
As you can see in the start
function we will be storing the video files in our webroot.
Updating The View
Now we have the streaming logic ready to go in our controller let’s set up the view so our clients can actually get the videos. For now, all we’ll do is attach a watch button to the video record in the table. Watch the blog for part 2 where I update the look of the video streamer!
Let’s update the view template. Open app/templates/Videos/view.php
and add
<div class="column-responsive column-80">
<div class="videos view content">
<h3><?= h($video->title) ?></h3>
<video controls preload="auto" src="<http://localhost:8765/videos/start/><?= $video->file_name ?>" width="100%"></video>
//...
</div>
</div>
As you can see the HTML5 video tag is taking a call to our function. This works because the src tag expects video input which the streaming logic outputs.
Uploading a video
Now that our application is set up to stream videos let's upload one and test it out!
Go to http://localhost:8765/videos/add while your server is running and you should get this screen
From here you can title the video what you want. The thumbnail location can’t be empty, but we’re not using it in this part. Check back here for part 2 where we add manual and automatic thumbnail selection! The file name must be the same exact filename as the file you want to stream. Caps and everything
Now you should be redirected to the index where you can see your new video waiting to be streamed
Again File Location → File Name
The last thing we have to do before we’re actually streaming video is put our video with the same name in app/webroot/img
Once you’ve done that go back to your index page, click view, and enjoy the show!
Conclusion
Thanks for making it to the end! Questions, comments, or concerns? Comment them down below and I’ll answer them as soon as possible.
Don’t forget to subscribe for more interesting posts like this!
Originally published at https://cakephp.blog on October 15, 2020.