Categories
Tutorial

Stream Video with CakePHP pt.1

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 designing 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 setup to stream videos lets 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 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 were 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!

Subscribe and Get CakePHP in Your Inbox!

Leave a Reply