JavaScript+PHP實現影片檔案分片上傳

發表於2024-02-27

摘要

影片檔案分片上傳,整體思路是利用JavaScript將檔案切片,然後迴圈呼叫上傳介面 upload.php 將切片上傳到伺服器。這樣將由原來的一個大檔案上傳變為多個小檔案同時上傳,節省了上傳時間,這就是檔案分片上傳的其中一個好處。

image.png

上程式碼

index.html

透過前端將檔案物件切分成多個小塊,然後依次將這些小塊的檔案物件上傳到伺服器。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>影片檔案分片上傳</title>
        <style>
            *{
                padding: 0;
                margin: 0;
            }
            .title {
                text-align: center;
                font-size: 25px;
                margin-top: 50px;
            }
            .video_upload {
                width: 500px;
                height: 60px;
                background: #eee;
                margin: 30px auto 0;
                border: 2px dashed #ccc;
                border-radius: 10px;
                position: relative;
                cursor: pointer;
                text-align: center;
                font-size: 25px;
                line-height: 60px;
                color: #666;
            }
            #fileInput {
                width: 100%;
                height: 100%;
                position: absolute;
                left: 0;
                top: 0;
                opacity: 0;
                cursor: pointer;
            }
            #uploadButton {
                width: 130px;
                height: 40px;
                border: none;
                outline: none;
                border-radius: 10px;
                font-size: 17px;
                margin: 10px auto;
            }
            #ret {
                text-align: center;
                font-size: 16px;
                margin-top: 20px;
            }
            #ret video {
                width: 450px;
            }
        </style>
    </head>
    <body>
        
        <p class="title">javaScript+PHP實現影片檔案分片上傳</p>
        <div class="video_upload">
            <span class="text"> + </span>
            <input type="file" id="fileInput" accept="video/*">
        </div>
        <button id="uploadButton" style="display:none;">開始上傳</button>
        <p id="ret"></p>

        <script>
        
            // 定義全域性變數
            let videoFile = null;
            let chunkSize = 1024 * 1024; // 1MB 分片大小
            
            // 當檔案選擇框的值改變時觸發該函式
            function handleFileSelect(event) {
                const fileList = event.target.files;
                if (fileList.length > 0) {
                    videoFile = fileList[0];
                    console.log("選擇了檔案: ", videoFile.name);
                    document.querySelector('.video_upload .text').textContent = videoFile.name;
                    document.querySelector('#uploadButton').style.display = 'block';
                }
            }
            
            // 分片並上傳檔案
            async function uploadFile() {
                if (!videoFile) {
                    console.error("請選擇一個影片檔案");
                    return;
                }
            
                const fileSize = videoFile.size;
                let start = 0;
                let end = Math.min(chunkSize, fileSize);
                let chunkIndex = 0;
            
                // 獲取檔名
                const fileName = videoFile.name;
            
                while (start < fileSize) {
                    const chunk = videoFile.slice(start, end); // 從檔案中擷取一個分片
            
                    // 使用FormData來構建multipart/form-data格式的請求體
                    const formData = new FormData();
                    formData.append('file', chunk);
                    formData.append('chunkIndex', chunkIndex);
                    formData.append('fileName', fileName); // 將檔名作為 formData 的一部分
            
                    try {
                        const response = await fetch('upload.php', {
                            method: 'POST',
                            body: formData
                        });
            
                        if (!response.ok) {
                            throw new Error('上傳失敗');
                        }
            
                        console.log('上傳分片 ', chunkIndex, ' 成功');
                    } catch (error) {
                        console.error('上傳分片 ', chunkIndex, ' 失敗: ', error.message);
                        return;
                    }
            
                    start = end;
                    end = Math.min(start + chunkSize, fileSize);
                    chunkIndex++;
                }
            
                console.log('檔案上傳完成');
            
                // 上傳完成後傳送通知給伺服器進行合併
                notifyServerForMerge(fileName);
            }
            
            // 傳送通知給伺服器進行合併
            async function notifyServerForMerge(fileName) {
                try {
                    const response = await fetch('merge_chunks.php', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json'
                        },
                        body: JSON.stringify({ fileName: fileName })
                    });
            
                    if (!response.ok) {
                        throw new Error('無法通知伺服器進行合併');
                    }
                    
                    const res_data = await response.json();
            
                    console.log('已通知伺服器進行合併');
                    document.querySelector('.video_upload .text').textContent = '分片合併完成!';
                    document.querySelector('#ret').innerHTML = '<video autoplay controls src="'+res_data.filePath+'"></video>';
                    document.querySelector('#uploadButton').style.display = 'none';
                } catch (error) {
                    console.error('通知伺服器進行合併時發生錯誤: ', error.message);
                }
            }
            
            // 註冊檔案選擇框的change事件
            document.getElementById('fileInput').addEventListener('change', handleFileSelect);
            
            // 註冊上傳按鈕的click事件
            document.getElementById('uploadButton').addEventListener('click', uploadFile);
        </script>

    </body>
</html>

upload.php

這個是用於接收前端傳過來的每一段分片,然後上傳到 uploads 資料夾,上傳之後就是一段一段的小分片。

<?php

    // 設定允許跨域訪問
    header("Access-Control-Allow-Origin: *");
    header("Access-Control-Allow-Methods: POST");
    
    // 檢查是否接收到檔案和分片索引
    if (isset($_FILES['file']['error']) && isset($_POST['chunkIndex']) && isset($_POST['fileName'])) {
        
        $error = $_FILES['file']['error'];
        $chunkIndex = $_POST['chunkIndex'];
        $fileName = $_POST['fileName']; // 獲取檔名
        
        // 檢查是否有錯誤
        if ($error !== UPLOAD_ERR_OK) {
            http_response_code(500);
            echo json_encode(array(
                'error' => '檔案上傳失敗'
            ));
            exit();
        }
        
        // 設定儲存目錄和檔名
        $uploadDir = './uploads/';
        $filePath = $uploadDir . $fileName . '.' . $chunkIndex;
        
        // 將分片移動到指定的目錄
        if (move_uploaded_file($_FILES['file']['tmp_name'], $filePath)) {
            
            echo json_encode(array(
                'success' => '分片上傳成功'
            ));
        } else {
            
            http_response_code(500);
            echo json_encode(array(
                'error' => '分片上傳失敗'
            ));
        }
    } else {
        
        http_response_code(400);
        echo json_encode(array(
            'error' => '缺少檔案、分片索引或檔名'
        ));
    }
    
?>

merge_chunks.php

這個是用來合併分片的,當前端完成上傳分片的操作,前端會非同步告訴伺服器你已經完成所有分片的上傳,接下來將每個分片名告訴合併程式完成所有分片的合併,合併之後就是一個完整的影片檔案。

<?php

    // 設定允許跨域訪問
    header("Access-Control-Allow-Origin: *");
    header("Access-Control-Allow-Methods: POST");
    header("Content-Type: application/json");
    
    // 獲取請求體中的檔名
    $data = json_decode(file_get_contents("php://input") , true);
    $fileName = isset($data['fileName']) ? $data['fileName'] : null;
    if ($fileName) {
        
        $uploadDir = './uploads/';
        $finalFilePath = $uploadDir . $fileName;
        $totalChunks = count(glob($uploadDir . $fileName . '.*'));
        
        // 檢查是否所有分片都已上傳
        if ($totalChunks > 0) {
            
            // 所有分片都已上傳,開始合併
            $finalFile = fopen($finalFilePath, 'wb');
            
            // 逐個讀取分片並寫入最終檔案
            for ($i = 0; $i < $totalChunks; $i++) {
                $chunkFilePath = $uploadDir . $fileName . '.' . $i;
                $chunkFile = fopen($chunkFilePath, 'rb');
                stream_copy_to_stream($chunkFile, $finalFile);
                fclose($chunkFile);
                unlink($chunkFilePath); // 刪除已合併的分片
                
            }
            
            fclose($finalFile);
            http_response_code(200);
            echo json_encode(array(
                'success' => '檔案合併成功',
                'filePath' => $finalFilePath
            ));
        } else {
            
            http_response_code(400);
            echo json_encode(array(
                'error' => '沒有上傳的分片'
            ));
        }
    } else {
        
        http_response_code(400);
        echo json_encode(array(
            'error' => '缺少檔名'
        ));
    }
?>

程式目錄

請自行建立 uploads 目錄。
image.png

作者

TANKING

相關文章