1

I am currently working on my website that supports file uploading. Part of this feature involves the ability to stream video from my site using the video tag in html. Currently I have this feature working flawlessly on desktop browsers such as chrome, but the second I try to stream a video on mobile, it simply doesn't load at all.

I have uploaded a sample video, and encourage anyone willing to help me to test it out themselves to see what im dealing with: https://upload.hellin.club/view-file.php?uuid=9034e9c1-1615-11f0-b42f-801844e26260&name=file_example_MP4_480_1_5MG.mp4

If anyone could help me fix this issue, id be very grateful!

Also here is my PHP script for viewing a file (which includes streaming a video):

<?php
include_once "includes/sys.php";
set_session_params();
session_start();

include_once "includes/getter.php";
$uuid = get_or_else("uuid");

$iframe = !isset($_GET["iframe"]) || $_GET["iframe"] == "true";
$url = get_url(sub_domain());

if ($uuid == null) {
    header("location: " . $url . "search-files.php");
    return;
}

include_once "includes/general-db-queries.php";
$upload = get_upload($uuid);

if ($upload == null) {
    header("location: " . $url . "search-files.php");
    return;
}

if ($upload->require_logged_in()) {
    include_once "includes/verify-request.php";
    if (!verify_request(false)) {
        return;
    }

    include_once "includes/user.php";
    $user = get_user();

    $available_uploads = get_available_uploads($user);
    $all_uploads = array_merge(...array_values($available_uploads));

    if (empty(array_filter($all_uploads, fn($obj) => $obj->uuid === $upload->uuid))) {
        header("location: " . $url . "search-files.php");
        return;
    }
}

include_once "includes/sanitize.php";

$path = $upload->file_path;
if (!file_exists($path)) {
    http_response_code(404);
    die("File not found");
}

$mime_type = $upload->file_type;
if (!$mime_type) {
    $mime_type = mime_content_type($path) ?: "application/octet-stream";
}

$file_name = sanitize($upload->file_name);
$is_image = $upload->is_image();
$is_text = $upload->is_text();
$viewable = $is_text || $is_image || $upload->is_audio() || $upload->is_video();
$serve_as_iframe = $iframe && $viewable;

if (!$serve_as_iframe) {
    while (ob_get_level()) ob_end_clean();

    $is_encrypted = $upload->encryption_key !== null;

    $file_contents = null;
    $file_size = filesize($upload->file_path);

    if ($is_encrypted) {
        $file_contents = $upload->get_raw_contents();
        $file_size = strlen($file_contents);
    }

    header('Content-Description: File Transfer');
    header('Content-Type: ' . ($is_text ? "text/plain" : $mime_type));
    header('Content-Disposition: inline; filename="' . $file_name . '"');
    header('Expires: 0');
    header('Cache-Control: must-revalidate');
    header('Pragma: public');
    header('Content-Length: ' . $file_size);
    header("Access-Control-Allow-Origin: $url");
    header('Access-Control-Allow-Credentials: true');
    header('Access-Control-Allow-Methods: GET');
    header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
    header('Access-Control-Max-Age: 0');
    header("Content-Security-Policy: script-src 'none'; object-src 'none'; frame-src 'self' $url;");
    
    $chunk_size = 102400;

    if (isset($_SERVER['HTTP_RANGE']) && !$is_encrypted) {
        list($unit, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
        if ($unit == 'bytes') {
            list($range_start, $range_end) = explode('-', $range);
            $range_start = intval($range_start);
            $range_end = $range_end ? intval($range_end) : $file_size - 1;

            header('HTTP/1.1 206 Partial Content');
            header("Content-Range: bytes $range_start-$range_end/$file_size");
            header('Content-Length: ' . ($range_end - $range_start + 1));

            $handle = fopen($path, 'rb');
            fseek($handle, $range_start);
            while (!feof($handle) && ftell($handle) <= $range_end) {
                echo fread($handle, $chunk_size);
                flush();
            }
            fclose($handle);
            exit;
        }
    }

    if ($upload->is_image() || $upload->is_text()) {
        echo $upload->get_raw_contents();
    } else if ($is_encrypted) {
        $offset = 0;
        while ($offset < $file_size) {
            echo substr($file_contents, $offset, $chunk_size);
            flush();
            $offset += $chunk_size;
        }
    } else {
        $handle = fopen($path, 'rb');
        if (!$handle) {
            http_response_code(500);
            die("Failed to open file.");
        }

        while (!feof($handle)) {
            echo fread($handle, $chunk_size);
            flush();
        }

        fclose($handle);
    }

    if ($upload->one_use) {
        $upload->delete();
    } else {
        $upload->increment_download_count();
    }

    return;
}

$stylesheet_version = filemtime("assets/style.css");

echo "<head>";
echo    "<link rel='stylesheet' type='text/css' href='assets/style.css?v=$stylesheet_version' />";
echo    "<meta name='viewport' content='width=device-width, initial-scale=1.0'>";

include_once "includes/config-reader.php";
$website_name = get_website_name();
$name = $upload->file_name;

echo    "<title>$website_name - $name</title>";
echo "</head>";

echo "<body class='user-page'>";
echo    "<script src='assets/scripts/theme.js'></script>";

include "includes/header.php";

$group = $upload->upload_group_uuid ?? "00000000-00000000-00000000-00000000";

echo    "<a class='button' id='back-button' href='" . $url . "search-files.php?group=$group'>Back</a>";
echo    "<div class='content flex-col flex-gap'>";

if ($is_image) {
    echo        "<img src='view-file.php?uuid=$uuid&iframe=false' class='view-img'/>";
} else if ($upload->is_video()) {
    echo        "<video controls='' preload='metadata' autoplay='' class='video-player' data-uuid='$uuid' name='media'><source src='view-file.php?uuid=$uuid&iframe=false' type='$mime_type'/>Your browser does not support this.</video>";
} else {
    echo        "<iframe src='view-file.php?uuid=$uuid&iframe=false' class='full-screen' sandbox='allow-scripts'></iframe>";
}

echo    "</div>";
echo "</body>";

if ($upload->one_use) {
    $upload->delete();
} else {
    $upload->increment_download_count();
}
?>
5
  • 1
    It's probably helpful when you specify what you mean by "mobile". Which OS? Which version? And for completeness perhaps also mention the browsers, and versions of those browsers, you tested. You could also reduce the amount of code in your question to just the relevant bit: The HTML with the video tag (and test it before you publish it here). Commented Apr 11 at 13:46
  • 1
    Certain browsers can be picky, when it comes to HTTP headers, specifically relating to caching. Also, Content-Description and Content-Disposition: inline might not be helpful, when you want to stream the file. Commented Apr 11 at 13:50
  • This is the current support for the <video> tag in browsers: caniuse.com/video It should be fine, although Safari has some minor restrictions. Commented Apr 11 at 13:52
  • 1
    If the request was a range request, you are sending 'Content-Length: ' . $file_size first, and then 'Content-Length: ' . ($range_end - $range_start + 1) after - clients might not like two conflicting Content-Length headers. Either restructure this to only send the header when you know what the actual length will be; or try to set the second parameter of header to true, so that it will replace a previously set header of the same name. Commented Apr 11 at 13:53
  • autoplay without muted might not work Commented Apr 11 at 15:09

1 Answer 1

1

Thank you for all your feedback. I tried everything you all suggested and unfortunately none of it seemed to fix the issue. After further testing I discovered that it wasn't specifically mobile that was the issue, but safari in general.

I have successfully got it to work. The issue was happening due to incorrectly handling the range provided by the browser.

I changed that part of the code from this:

while (!feof($handle) && ftell($handle) <= $range_end) {
    echo fread($handle, $chunk_size);
    flush();
}

To this:

while ($range_start <= $range_end && !feof($handle)) {
    $remaining_range = $range_end - $range_start + 1;
    $bytes_to_read = min($remaining_range, $chunk_size);
    echo fread($handle, $bytes_to_read);
    $range_start += $bytes_to_read;
    flush();
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.