Using PHP as a (Terrible) Video Player
While browsing a forum, I noticed an ASCII art image, which made me wonder how such images are created. It turns out that the process is quite simple. All that is required is to iterate through an image’s pixels and replace them with ASCII characters. Darker pixels are replaced with characters that appear larger, while lighter pixels are replaced with smaller characters, such as a dot.
That led me to another thought: since a video is just a sequence of frames (images), could I encode every frame of a video into ASCII characters, effectively turning the video into an ASCII animation? And how difficult would that be to accomplish?
Extracting Frames from a Video
The first step is figuring out how to read a video frame by frame. I know PHP has built-in methods to read pixel data from an image, but how can I decode frames from a video? Apparently, PHP does not have libraries for video decoding by default, so I decided to use a popular encoding/decoding tool named FFmpeg.
The idea is to use FFmpeg to decode the video frame by frame, then pass those frames to a PHP application. The application will iterate over each pixel of a frame and replace it with an ASCII character. The resulting ASCII frames can then be displayed in the terminal or saved to a file for later viewing.
Using FFmpeg to Retrieve Frames
Let’s start by constructing an FFmpeg command with arguments to extract frames one by one. The most important flags include:
- Hardware acceleration (depending on the operating system)
- Grayscale conversion, since we are not displaying colors in the terminal, which simplifies choosing the appropriate ASCII characters
- Frame encoding as PNG images, since PNG is lossless and maintains the necessary details
FFmpeg uses stdout to return frame data and stderr to print encoding status. The -loglevel error
flag ensures that only errors are printed to the error stream, as the status messages are not needed.
$hwaccel = match (true) {
stristr(PHP_OS, 'linux') => " -hwaccel vaapi -vaapi_device /dev/dri/renderD128 ",
stristr(PHP_OS, 'darwin') => " -hwaccel videotoolbox ",
stristr(PHP_OS, 'win') => " -hwaccel dxva2 ",
default => "",
};
$cmd = "\"$this->ffmpegPath\" $hwaccel -i " . escapeshellarg($videoPath) .
" -vf \"fps={$settings['framerate']},scale={$settings['resolution']},format=gray\" " .
" -fflags nobuffer -loglevel error -flush_packets 1 -update 1 " .
" -f image2pipe -pix_fmt gray -vcodec png -";
Processing Frames in PHP
Now, let’s start the FFmpeg process with the built command and open pipes to receive frame data from the FFmpeg process.
$process = proc_open($cmd, [
0 => ["pipe", "r"],
1 => ["pipe", "w"],
2 => ["pipe", "w"]
], $pipes);
Next, we read from $pipes[1]
, which corresponds to STDOUT. This is where the PNG-encoded frame data is received from the FFmpeg process. Since we might receive only a portion of a frame at a time, we accumulate chunks of data into a buffer. Once the buffer contains a complete PNG image, it is passed for further processing.
$buffer = '';
// Process each frame
while (true) {
$status = proc_get_status($process);
if (!$status['running'] && feof($pipes[1])) {
break;
}
// Read data from FFmpeg output
if (($chunk = fread($pipes[1], 4096)) !== false && $chunk !== '') {
$buffer .= $chunk;
}
// Process complete PNG images in the buffer
$buffer = $this->processFrames($buffer, $frameCallback);
}
Converting Frames to ASCII
This is the heart of the application. The accumulated PNG buffer is turned into a GdImage object, which PHP can use to access pixel data. Instead of iterating over every single pixel, we divide the image into horizontal and vertical blocks. Each block’s brightness is determined by sampling the pixel at its center. Since ASCII characters are larger than pixels, this approach provides a reasonable approximation of the image.
We then map the brightness value to a corresponding character from a predefined grayscale ASCII character set. The process continues line by line until the entire frame is converted into ASCII characters.
$grayCharacters = "$@B%8&WM#*+=-:. "; // ASCII gradient from dark to light
$image = @imagecreatefromstring($frameData);
$asciiFrame = [];
for ($y = 0; $y < $newHeight; $y++) {
$row = '';
$baseY = $y * $this->widthSubsample;
for ($x = 0; $x < $newWidth; $x++) {
$baseX = $x * $this->heightSubsample;
// Sample center pixel for each block
$sampleX = min($baseX + intdiv($this->heightSubsample, 2), $width - 1);
$sampleY = min($baseY + intdiv($this->widthSubsample, 2), $height - 1);
$grayValue = imagecolorat($image, $sampleX, $sampleY);
// Map color value to ASCII character
$index = (int) ceil((strlen($grayCharacters) - 1) * $grayValue / 255);
$row .= $grayCharacters[$index];
}
$asciiFrame[] = $row;
}
Storing and Displaying ASCII Frames
Once an ASCII frame is generated, it can be displayed in the terminal or saved to a file. To make the ASCII video file smaller, I used PHP’s gzcompress
function to compress frame data.
$compressed = gzcompress($asciiFrame);
fwrite($file, base64_encode($compressed) . "\n");
Results and Conclusion

To test compression and playback, I used the open-source animated movie Big Buck Bunny. The original 691 MB video file was compressed into a 52 MB ASCII movie file. Playback worked without any issues — I only needed to reduce the terminal font size using Ctrl
+ -
to fit more details on the screen.
During this project, I also developed a PHP library named Scoria for ASCII video encoding, decoding, and playback. You can find the code on GitHub.
While I can’t imagine watching ASCII movies regularly, this was a fun and unusual project to experiment with!