In this article, we will build a simple asynchronous video streaming server with ReactPHP. ReactPHP is a set of independent components which allows you to create an asynchronous application in PHP. ReactPHP Http is a high-level component which provides a simple asynchronous interface for handling incoming connections and processing HTTP requests.
The core of every ReactPHP application is the event loop. It is the most low-level component. Every other component uses it. Event loop runs in a single thread and is responsible for scheduling asynchronous operations. So, the most of ReactPHP applications have the following structure:
- We init the loop.
- Set up the world (configure other components that use the loop).
- Run the loop.
When we run the loop, the application starts execution of the asynchronous operations. The execution runs endlessly, until we call
$loop->stop() to stop it or terminate the script itself.
In our case, the Set up the world step looks the following:
- Create a server (
React\Http\Server) for handling incoming requests.
- Create a socket (
React\Socket\Server) to start listening for the incoming connections.
At first, let’s create a very simple
Hello world server to understand how it works:
The main logic of the server is placed in the callback, which is passed to the server constructor. This callback is being executed for each incoming request. It accepts an instance of the
Request object and returns
Response object. The
Response class constructor accepts the response code, headers and the body of the response. In our case, for each request, we return the same static string
If we execute this script it will run endlessly. The server is working and is listening to the incoming requests. If we now open
127.0.0.1:8000 in the browser we will see our
Hello world response. Nice!
Simple Video Streaming
Now, we can try something more interesting.
React\Http\Response constructor can accept an instance of ReactPHP ReadableStreamInterface as a response body. A readable stream is used to read data from the source in a continuous fashion, instead of loading the whole file into the memory. This allows us to stream data directly into the response body. Check this article if you want to know more about ReactPHP streams.
For example, we can open file
bunny.mp4 (you can download it from the Github) in a read mode, create a
ReadableResourceStream with it and then provide this stream as a response body like this:
Note that this example uses
fopen()for simplicity and demo purposes only! This should not be used in a truly asynchronous application because the filesystem is inherently blocking and each call could potentially take several seconds. Read this tutorial in case you need to work asynchronously with the filesystem in ReactPHP ecosystem.
To create an instance of the
ReadableResourceStream we need an event loop, so we need to pass it to the closure. We also have changed
Content-Type header to
video/mp4 to notify our browser that we are sending a video in the response. There is no need to specify a
Content-Length header because behind the scenes ReactPHP will automatically use chunked transfer encoding and send the respective header
Now refresh the browser and watch the streaming video:
Really cool! We have a streaming video server with several lines of code!
Notice. It is important to create an instance of the
ReadableResourceStream right in the callback of the server. Remember the asynchronous nature of our application. If we create the stream outside of the callback and then simply pass it into the callback, there will be no streaming at all. Why? Because the process of reading a video file and processing the incoming requests to the server both work asynchronously. That means that while the server is waiting for new connections we also start reading a video file. To prove this we can use stream events. Every time a readable stream receives data from its source it fires
data event. We can attach a handler to this event and every time when we read data from the file, we will output a message:
When execution reaches the last line
$loop->run(); the server starts listening to the incoming requests and we also start reading a file:
So, chances high that when the first request arrives at the server we have already reached the end of the video file and there is no data for streaming. When the request handler receives a response stream that is already closed, it will simply send an empty response body, which means in our case no video streaming and an empty page in the browser.
On the next step, we can improve a little our server. Let’s say that a user can specify in the query string a file name to be streamed. For example, when users type in the browser:
http://127.0.0.1:8000/?video=bunny.mpg the server starts streaming file
bunny.mpg. We will store our files for streaming in
media directory. Now we need somehow to get the query parameters from the request. Request object that we receive in the request handler has method
getQueryParams which returns an array of the GET query, similar to global variable
Now to view
bunny.mpg video, we can visit
http://127.0.0.1:8000?video=bunny.mp4 in the browser. The server checks the incoming request for GET parameters. If it finds
video parameter we assume that it is a video file name, which user wants to be streamed. Then we build a path to this file, open a readable stream and pass it to the response. But there are three issues here. Do you see them?
- What if there is no such file on server? We should return 404 page in this case.
- Now we have a hardcoded
Content-Typeheader value. We should determine it according to the specified file.
- A user can request any file on the server. We should allow to request only certain files.
Checking if file exists
Before opening a file and creating a stream we should check if this file exists on the server. If not we simply return
Notice that we can’t use
file_exists here, because it will be a blocking call and opens the chance for race conditions. That’s why I simply mute errors when opening a stream and then check if a stream was opened.
Now our server doesn’t crash when a user requests a wrong file. It responds with a correct message:
Determining file mime type
And again there can be a temptation to use a nice
mime_content_type() function, which returns MIME type for a file. But it will be a blocking call and it is not recommended to be used when running an event loop. So, in our case we can use something custom, like this:
This is a simple helper function with a pre-defined map of extensions and appropriate types. If a passed filename ends with one of the predefined extensions it returns a type, otherwise it returns
Very nice, we have removed a hardcoded
Content-Type header value and now it is determined automatically according to the file.
Allowing to request only certain files
But still, there is an issue with requesting files. A user can request any file on the server. For example, if our server code is located in
server.php file and we check this URL in the browser
http://127.0.0.1:8000/?video=../server.php the result will be the following:
Not very secure… To fix it, we can use
basename function to grab only the file name from the request and cut out the path if it was specified:
Now for the same URL we will receive 404 page. Fixed.
Actually, the server is ready, but the main logic, which is placed in the request handler doesn’t look very nice. Of course, if you are not going to change or extend it, you can keep it as it is, right in a callback. But if the server logic is going to change, for example instead of a plain text we would like to render some HTML pages this callback will grow and very soon it will become hard to understand and maintain. Let’s make some refactoring and extract this logic into its own
VideoStreaming class. To be able to use this class as a callable request handler we should implement a magic
__invoke() method in it. And then we can simply pass an instance of this class as a callback to the
Now we can start building
VideoStreaming class. It requires a single dependency - an instance of the event loop which will be injected through the constructor. At first, we can simply copy-and-paste the code from a request callback into the
__invoke() method and then start refactoring it:
Next, we can refactor this
__invoke method. Let’s figure out what is happening here:
- We parse the request query and determine the file which user has requested.
- Create a stream from this file and send it back as a response.
So, it looks like we can extract two methods here:
getFilePath is very simple. We receive request parameters with
$request->getQueryParams() method. Then if there is no
file key there we simply return an empty string, which means that a user has opened the server without any GET parameters. In this case, we could show a static page or something like this. Now we return a simple plain text message
Video streaming server. If a user has specified
file in GET request we create a path to this file and return it:
makeResponseFromFile will be also very simple. If there is no file for the given path we immediately return a 404 response. Otherwise, we open this file, create a readable stream and return it as a response body:
getMimeTypeByExtension() helper function to a method to keep all things closer to each other:
Here is a full code of the
Of course, instead of a simple request handler callback now we have 3 times more code, but if this code is going to be changed in the future it will be much easier to make these changes. I have left
getMimeTypeByExtension() helper function as it is, but it can be also moved to a method.
You can find examples from this article on GitHub.
This article is a part of the ReactPHP Series.