How video streaming works on the web

Last updated : Sep 11, 2022

Streaming is defined as transmissions of audio/video (data) in the continuous flow over wired/wireless medium. This definition is taken from Verizon's website.

In this article you will know streaming beyond this definition and in an easy way and also in addition you will get some base scripts so you can use them and modify them in the best way to create your own streaming platform in a couple of hours.

The very basic idea of streaming is the process of transferring the data in a file from server to client in packets and while this is going on also playing the file on client system simultaneously.

Live streaming: live streaming is almost same as streaming but the little difference is, in live streaming a temporary file is made on server and that file keeps appending data from the broadcaster side and broadcasts data to the clients connected to that live streaming. That’s enough for streaming and live streaming.

Now let’s see what we are going to do in this article for creating a streaming portal.

The idea is there will be some files on the server and there is a server side code written in PHP. It will calculate what size of data should be sent in the form of packet. It will send the streamed data with following headers:

1. Content-type
2. Cache-control
3. Expires
4. Accept-ranges
5. 206 partial content
6. Content-length

Note: Always remember when you are working on any platform which includes streaming, “header 206 partial content” is necessary to be included. This header tells the browser that additional data will be coming in some time.

And at the frontend side we will be using simple Javascript code bind with React component. The logic for frontend is simple fetch request will get initial data and will be added in progressive buffer array and in media source object of Javascript. And subsequent fetch requests will get more data and will be appended in media source object on waiting event of HTML’s media player until and unless the content length reaches to the end of file.

Let's get going with code now. First we will go with backend code in PHP

<?php
    class VideoStream
    {
        private $path = "";
        private $stream = "";
        private $buffer = 1024 * 1024;
        private $start  = -1;
        private $end    = -1;
        private $size   = 0;
     
        function __construct($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/webm");
            // header("Content-Type: text/plain");
            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);
            header("Accept-Ranges: bytes");
             
            if (isset($_SERVER['HTTP_RANGE'])) {

                // echo $_SERVER['HTTP_RANGE'];
      
                $c_start = $this->start;
                $c_end = $this->end;
     
                list(, $range) = explode('=', $_SERVER['HTTP_RANGE'],2);
                // print_r($range); die();
                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 = (isset($range[1]) && is_numeric($range[1])) ? $this->buffer : $this->buffer;
                }
                $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;
                // $length = $this->end - $this->start;
                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;
            
            while(!feof($this->stream) && $i <= $this->end) {
                $bytesToRead = $this->buffer;
                if(($i+$bytesToRead) > $this->end) {
                    $bytesToRead = $this->end - $i + 1;
                }
                
                set_time_limit(0);
                // ob_clean();

                $data = fread($this->stream, $bytesToRead);
                
                echo $data;
                flush();
                
                $i += $bytesToRead;
            }
        }
         
        /**
         * Start streaming video content
         */
        function start()
        {
            $this->open();
            $this->setHeader();
            $this->stream();
            $this->end();
        }
    }

?>
require_once(__DIR__.'/VideoStreamer.php');
header('Access-Control-Allow-Origin: *');
header("Access-Control-Allow-Headers: *");
header("Access-Control-Expose-Headers: Content-Range");

	
if($_SERVER['REQUEST_METHOD'] == "GET"){
	if(isset($_GET['call_func'])){           
		
		if($_GET['call_func'] == "file_real_stream"){

			if(function_exists('apache_request_headers')){
				$headers = apache_request_headers();
				foreach($headers as $key => $val){
					if(($key == 'vid_title')||($key == 'Vid_title')){
						$is_header_info_found = 1;
						$vid_title_from_header = $val;
						break;
					}
				}
			}				
			$streamer = new VideoStream($video_path);
			$streamer->start();
		}
		else{
			// header("Content-type: application/json");
			echo json_encode([
				'status' => false,
				'msg' => 'func val present but no match in get request'
			]);
		}
	}
	else{
		header("Content-type: application/json");
		echo json_encode([
			'status' => false,
			'msg' => 'func val not present in get request'
		]);
	}
}

This is a simple code for handling requests for our server.

Now we will include the frontend code in React(Javascript)

<video autoPlay="false" id='video_cont' controls="true" muted="true"></video>

Simple HTML Video element is good to go with basic streaming service.

//vars for playing video
var buffered_bytes = 0;
var initial_buffer = 524288;
var local_buffer = (1048576);//1048576;//2097152;
var total_content_length = 0;
var vid_title = `video file name.webm`;
var req_url = `you server file path?call_func=file_real_stream&vid=video file name.webm`;
var duration = 0;
var delta = 0;
var vid_blob = null;
var vid_timer = 0;
var sourceBuffer = null;
var mediaSource = null;
var progressive_buffer = [];

buffered_bytes: the variable which will keep track of content length that has been streamed.
initial_buffer: the variable which will be used for initial buffer which will be returned by the server.
local_buffer: the content length will be used for subsequent HTTP requests.
total_content_length: the total content length which will will from initial request as response header.
vid_title: the video title to pass with request.
req_url: the request url
vid_blob: the blob which we will be returned by server.
sourceBuffer: sourcebuffer type
mediaSource: media source object
progressive_buffer: progressive_buffer is an array for storing next streamed data for instant play.

vidElement = document.getElementById('video_cont');
mediaSource = new MediaSource();
vidElement.src = URL.createObjectURL(mediaSource);
setSourceBuffer();
loadInitialStream();

vidElement is initialized with HTML element and media source is initialized and vidElement is put with a source of objecturl created by a function createObjectURL() with mediasource as a parameter.
Then there are 2 functions one for setting source buffer in media source object and the other for loading initial stream.

// promise for adding source buffer to media source
var setSourceBuffer = async () => {
	sourceBuffer = await new Promise((resolve, reject) => {
		const getSourceBuffer = () => {
			try {
				// var mime = 'video/mp4; codecs="aac,h.264"';
				// var mime = 'video/mp4; codecs="avc1.4d401f, mp4a.40.2"';
				// var mime = 'video/mp4; codecs="avc1.4D4001,mp4a.40.2"';
				var mime = 'video/webm; codecs="vp9,opus"';
				const sourceBuffer = mediaSource.addSourceBuffer(mime);
				// sourceBuffer.mode = 'sequence';
				// log_msg('buffer added');
				resolve(sourceBuffer);
			} catch (e) {
				reject(e);
			}
		};
		if (mediaSource.readyState === 'open') {
			getSourceBuffer();
		} 
		else {
			mediaSource.addEventListener('sourceopen', getSourceBuffer);
		}
	});
}

Here setSourceBuffer() function includes a promise which is adding the types of mime which the streaming platform will support. Here we are adding codecs: vp9 and opus. These codecs are supported by .webm files.

Important point to note:
In this streaming platform we are only playing .webm files because the codecs are easily supported by every browser. If you want us to provide code for running .mp4 files as well, please do comment and we will formulate a way to run .mp4 files easily and will add in this article itself.

//function for loading first buffer of video
async function loadInitialStream(){
	try{
		var response = await fetch(req_url,{
			headers: {
				'Range': 'bytes='+buffered_bytes+"-"+initial_buffer,
				'vid_title': vid_title
				// 'Range': 'bytes=1048577-2097152'
			}
		});
		vid_blob = await response.blob();
		var vid_buff = await vid_blob.arrayBuffer();
		total_content_length = response.headers.get('Content-Range');
		total_content_length = total_content_length.substr((total_content_length.indexOf('/')+1),total_content_length.length);
	
		sourceBuffer.appendBuffer(vid_buff);

		//updating buffered_bytes
		buffered_bytes = buffered_bytes + initial_buffer;
	}
	catch(ex){
		log_msg('initial buffer err: '+ex);
	}	


	sourceBuffer.addEventListener('updateend', function() {
		// log_msg('update end');
		// log_msg(mediaSource.updating);
		// log_msg(mediaSource.readyState);
		//if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
			// mediaSource.endOfStream();
			// log_msg('update end & end of stram');
			
			if(vidElement.paused){
				// vidElement.playbackRate = 8.0;
				vidElement.play()
				.then(data => {
				})
				.catch(ex => {
				});
			}
		//}
	});		
}

In this snippet, initial stream loading is being done. First a request is sent to server with above mentioned url and vid_title and Range as header of the request. After the response, we will get blob of the stream and then array buffer to append that in sourcebuffer of mediasource. Then we are getting total_content_length from response's Content-Length header. Then finally append arrayBuffer to sourceBuffer.

lastly we have an event handler function because whenever sourcebuffer is appended then this event is fired called 'updateend' in this function we are finally playing the video.

// trigger for fetching next buffer
vidElement.addEventListener('waiting',async () => {
	if(progressive_buffer.length == 0){
		if((buffered_bytes) == (total_content_length-1)){
			log_msg('buffer complete');
			return;
		}
		if((buffered_bytes+local_buffer) > total_content_length){
			try{
				var response = await fetch(req_url,{
					headers: {
						'Range': 'bytes='+(buffered_bytes+1)+"-"+(total_content_length),
						'vid_title': vid_title
						// 'Range': 'bytes=1048577-2097152'
					}
				});
				vid_blob = await response.blob();
				var vid_buff = await vid_blob.arrayBuffer();

				progressive_buffer.push(vid_buff);

				var remaining_bytes = total_content_length - (buffered_bytes+1);
				
				(buffered_bytes) = (buffered_bytes) + (remaining_bytes);
			}
			catch(ex){
				log_msg('end buffer append err: '+ex);
			}
		}
		else {
			// log_msg(`start : ${parseInt(buffered_bytes+1)}`);
			try{
				var response = await fetch(req_url,{
					headers: {
						'Range': 'bytes='+(buffered_bytes+1)+"-"+(buffered_bytes+local_buffer),
						'vid_title': vid_title
						// 'Range': 'bytes=1048577-2097152'
					}
				});
				vid_blob = await response.blob();
				var vid_buff = await vid_blob.arrayBuffer();
				
				progressive_buffer.push(vid_buff);

				// if(!sourceBuffer.updating){
				// 	// log_msg('not updating');
				// 	sourceBuffer.appendBuffer(vid_buff);
				// }
				(buffered_bytes) = (buffered_bytes) + (local_buffer);
			}
			catch(ex){
				log_msg('mid buffer append err: '+ex);
			}
		}
		
	}
	else{
		try{
			if(!sourceBuffer.updating){
				// log_msg('not updating');
				sourceBuffer.appendBuffer(progressive_buffer[0]);
				progressive_buffer = [];
			}
		}catch(ex){
			//console.log(req_url+", length!=0 append error: "+ex);
		}
		if((buffered_bytes) == (total_content_length-1)){
			log_msg('buffer complete');
			return;
		}
		if((buffered_bytes+local_buffer) > total_content_length){
			try{
				var response = await fetch(req_url,{
					headers: {
						'Range': 'bytes='+(buffered_bytes+1)+"-"+(total_content_length),
						'vid_title': vid_title
						// 'Range': 'bytes=1048577-2097152'
					}
				});
				vid_blob = await response.blob();
				var vid_buff = await vid_blob.arrayBuffer();

				progressive_buffer.push(vid_buff);

				var remaining_bytes = total_content_length - (buffered_bytes+1);
				
				(buffered_bytes) = (buffered_bytes) + (remaining_bytes);
			}
			catch(ex){
				log_msg('end buffer append err: '+ex);
			}
		}
		else {
			// log_msg(`start : ${parseInt(buffered_bytes+1)}`);
			try{
				var response = await fetch(req_url,{
					headers: {
						'Range': 'bytes='+(buffered_bytes+1)+"-"+(buffered_bytes+local_buffer),
						'vid_title': vid_title
						// 'Range': 'bytes=1048577-2097152'
					}
				});
				vid_blob = await response.blob();
				var vid_buff = await vid_blob.arrayBuffer();
				
				progressive_buffer.push(vid_buff);

				// if(!sourceBuffer.updating){
				// 	// log_msg('not updating');
				// 	sourceBuffer.appendBuffer(vid_buff);
				// }
				(buffered_bytes) = (buffered_bytes) + (local_buffer);
			}
			catch(ex){
				log_msg('mid buffer append err: '+ex);
			}
		}
	}
});

The 'waiting' event of HTML's media player is fired when initial frame comes to an end and it waits for next frame to be loaded. In this event handler function we check that if progressive_buffer length is zero then we send a request and get streamed data and append in sourcebuffer and play otherwise we append data from progressive_buffer array to sourceBuffer and fetch next frame of data in background.

You can also watch video to better understand the streaming.



Sign in for comment. Sign in