使用js文件切片上传组件resumable.js上传视频

要写一个媒体文件切片上传,上传到server后FTP传输到远端server。FTP上传很简单,切片上传找到了resumable.js这个组件库。 写demo记录下。

前端

<fieldset>
  <legend>video preview</legend>
  <div>
    <video id="video-preview" controls muted height="112px" width="200px"></video>
    <input type="file" id="video-upload" accept="video/*" />
  </div>
</fieldset>
<script>
  document.getElementById("video-upload").addEventListener("change", function (e) {
    var file = e.target.files[0]; // 获取文件引用
    var videoElement = document.getElementById("video-preview"); // 获取video元素

    if (file && file.type.startsWith("video/")) {
      // 检查文件是否为视频格式
      var fileURL = URL.createObjectURL(file); // 创建文件URL
      videoElement.src = fileURL; // 设置video的src属性为文件URL
    } else {
      alert("请选择一个视频文件。"); // 提示用户选择正确的文件类型
    }
  });
</script>

<fieldset>
  <legend>video preview & upload</legend>
  <div>
    <video id="video-preview1" controls muted height="112px" width="200px"></video>
    <video id="video-preview2" controls muted height="112px" width="200px"></video>
    <a href="#" id="browseButton">Select files</a>
    <a href="#" id="uploadButton" onclick="beginUpload()">Upload</a>
    <span id="uploadprogress"></span>
  </div>
</fieldset>
<script src="../plugins/resumable.js"></script>
<script>
  // 可实例化多个
  const r = new Resumable({
    target: "../../php-demos/upload.php",
    chunkSize: 2 * 1024 * 1024,
    testChunks: false, //打开后查看chunk是否存在的get请求先于发送chunks的post请求发起,返回大量的404
    simultaneousUploads: 6,
    fileType: ["mp4", "webm", 'ogg'],
    query: {},
    maxFiles: 1,
  });
  r.assignBrowse(document.getElementById("browseButton"));

  //文件上传成功
  r.on("fileSuccess", function (file, msg) {
    const msg_obj = JSON.parse(msg);
    console.log("存储文件名", msg_obj["res_files"][0]);

    document.getElementById("video-preview2").src = "http://localhost:3000/php-demos/temp/" + msg_obj["res_files"][0];
  });

  r.on("fileProgress", function (file) {
    console.log("fileProgress", file);
  });
  r.on("fileAdded", function (file, event) {
    console.log("fileAdded", event, file);

    document.getElementById("video-preview1").src = URL.createObjectURL(file["file"]);
  });
  r.on("fileRetry", function (file) {
    console.log("fileRetry", file);
  });
  r.on("fileError", function (file, message) {
    console.log("fileError", file, message);
  });
  r.on("uploadStart", function () {
    console.log("uploadStart");
  });
  r.on("complete", function () {
    console.log("complete");
  });
  r.on("progress", function () {
    document.getElementById("uploadprogress").innerText = (r.progress() * 100).toFixed(2);
  });
  r.on("error", function (message, file) {
    console.log("error", message, file);
  });
  r.on("pause", function () {
    console.log("pause");
  });
  r.on("cancel", function () {
    console.log("cancel");
  });

  //上传
  function beginUpload() {
    r.upload();
  }
</script>

后端接收

查看代码
 <?php

date_default_timezone_set("Asia/Shanghai");

class Upload
{
    public static $temp_path = "./temp/";

    public static function write_log($str)
    {
        $log_str = "[" . date('Y-m-d H:i:s') . "]" . ": {$str}\r\n";
        if (($fp = fopen(self::$temp_path . 'upload_log.txt', 'a+')) !== false) {
            fputs($fp, $log_str);
            fclose($fp);
        }
    }

    public static function rrmdir($dir)
    {
        if (is_dir($dir)) {
            $objects = scandir($dir);
            foreach ($objects as $object) {
                if ($object != "." && $object != "..") {
                    if (filetype($dir . "/" . $object) == "dir") {
                        self::rrmdir($dir . "/" . $object);
                    } else {
                        unlink($dir . "/" . $object);
                    }
                }
            }
            reset($objects);
            rmdir($dir);
        }
    }

    public static function createFileFromChunks($temp_dir, $fileName, $chunkSize, $totalSize, $total_files)
    {
        $total_files_on_server_size = 0;
        $temp_total = 0;
        foreach (scandir($temp_dir) as $file) {
            $temp_total = $total_files_on_server_size;
            $tempfilesize = filesize($temp_dir . '/' . $file);
            $total_files_on_server_size = $temp_total + $tempfilesize;
        }

        $res_filename = '';
        if ($total_files_on_server_size >= $totalSize) {

            $pos = strrpos($fileName, '.');
            $name = substr($fileName, 0, $pos);
            $ext = substr($fileName, $pos);
            $filePath = $name . '-' . time() . '-' . rand(10, 99) . $ext;
            $res_filename = $filePath;

            if (($fp = fopen(self::$temp_path . $filePath, 'w')) !== false) {
                for ($i = 1; $i <= $total_files; $i++) {
                    fwrite($fp, file_get_contents($temp_dir . '/' . $fileName . '.part' . $i));
                    self::write_log($fileName . ' writing chunk ' . $i);
                }
                fclose($fp);
            } else {
                self::write_log($fileName . ' cannot create the destination file');
                return false;
            }

            if (rename($temp_dir, $temp_dir . '_UNUSED')) {
                self::rrmdir($temp_dir . '_UNUSED');
            } else {
                self::rrmdir($temp_dir);
            }
        }

        return $res_filename;
    }
}

if ($_SERVER['REQUEST_METHOD'] === 'GET') {

    if (!(isset($_GET['resumableIdentifier']) && trim($_GET['resumableIdentifier']) != '')) {
        $_GET['resumableIdentifier'] = '';
    }
    $temp_dir = Upload::$temp_path . $_GET['resumableIdentifier'];
    if (!(isset($_GET['resumableFilename']) && trim($_GET['resumableFilename']) != '')) {
        $_GET['resumableFilename'] = '';
    }
    if (!(isset($_GET['resumableChunkNumber']) && trim($_GET['resumableChunkNumber']) != '')) {
        $_GET['resumableChunkNumber'] = '';
    }
    $chunk_file = $temp_dir . '/' . $_GET['resumableFilename'] . '.part' . $_GET['resumableChunkNumber'];
    if (file_exists($chunk_file)) {
        header("HTTP/1.0 200 Ok");
    } else {
        header("HTTP/1.0 404 Not Found");
    }
}

$res_files = [];
if (!empty($_FILES)) {
    foreach ($_FILES as $file) {
        $res_filename = '';

        if ($file['error'] != 0) {
            Upload::write_log('error ' . $file['error'] . ' in file ' . $_POST['resumableFilename']);
            continue;
        }

        if (isset($_POST['resumableIdentifier']) && trim($_POST['resumableIdentifier']) != '') {
            $temp_dir = Upload::$temp_path .  $_POST['resumableIdentifier'];
        }
        $dest_file = $temp_dir . '/' . $_POST['resumableFilename'] . '.part' . $_POST['resumableChunkNumber'];

        if (!is_dir($temp_dir)) {
            mkdir($temp_dir, 0777, true);
        }

        if (!move_uploaded_file($file['tmp_name'], $dest_file)) {
            Upload::write_log('Error saving (move_uploaded_file) chunk ' . $_POST['resumableChunkNumber'] . ' for file ' . $_POST['resumableFilename']);
        } else {
            $res_filename = Upload::createFileFromChunks($temp_dir, $_POST['resumableFilename'], $_POST['resumableChunkSize'], $_POST['resumableTotalSize'], $_POST['resumableTotalChunks']);
        }

        $res_files[] = $res_filename;
    }
}

echo json_encode(['res_files' => $res_files]);

测试150MB的视频文件,分成75块,完成时间大约30s。

FTP上传输到远端服务器

 这里列出了FTP操作类

查看代码
 <?php

//视频文件上传到server后上传到NAS

class Ftp
{
    private $ip = 'ip';
    private $port = 'port';
    private $username = 'username';
    private $password = 'password';
    private $timeout = 5;
    private $conn = null;

    private $src_path = '../temp/';
    private $root_path = '/ESOP';
    private $remote_path = '';

    public function __construct($subPath)
    {
        $this->conn = ftp_connect($this->ip, $this->port, $this->timeout);
        if ($this->conn) {
            $login_result = ftp_login($this->conn, $this->username, $this->password);
            if ($login_result) {
                ftp_pasv($this->conn, true);

                $sub_path = $subPath;

                //查看远程目录是否存在,不存在则创建目录
                $this->remote_path = $this->root_path . '/' . $sub_path;
                $lists = ftp_nlist($this->conn, $this->remote_path);
                if (empty($lists)) {
                    $this->write_log($this->remote_path . '不存在');
                    $this->mkSubDirs($this->root_path, $sub_path);
                }
            } else {
                $this->write_log('FTP-' . $this->ip . '登录失败');
            }
        } else {
            $this->write_log('FTP-' . $this->ip . '连接失败');
        }
    }

    public function __destruct()
    {
        if ($this->conn) ftp_close($this->conn);
        $this->conn = null;
    }

    public function mkSubDirs($basedir, $path)
    {
        @ftp_chdir($this->conn, $basedir);
        $parts = explode('/', $path);
        foreach ($parts as $part) {
            if (!@ftp_chdir($this->conn, $part)) {
                ftp_mkdir($this->conn, $part);
                ftp_chdir($this->conn, $part);
            }
        }

        ftp_chdir($this->conn, '/');
        $this->write_log('FTP创建目录' . $basedir . '/' . $path);
    }

    public function upload_file($upload_file, $old_file_path)
    {
        //上传文件
        $src_file = $this->src_path . $upload_file;
        $dst_file = $this->remote_path . '/' . $upload_file;
        #使用二进制模式,而非FTP_ASCII模式,否则会导出传输过去的视频文件多字节而打开错误
        $res = ftp_put($this->conn, $dst_file, $src_file, FTP_BINARY);
        if ($res) {
            $this->write_log($upload_file . '传输成功');
        } else {
            $this->write_log($upload_file . '传输失败');
        }

        //删除远程的旧文件
        if (!empty($old_file_path)) {
            if (ftp_size($this->conn, $old_file_path) > 0) {
                $res = ftp_delete($this->conn, $old_file_path);
                if ($res) {
                    $this->write_log($old_file_path . '删除成功');
                } else {
                    $this->write_log($old_file_path . '删除失败');
                }
            }
        }

        $file_size = ftp_size($this->conn, $dst_file);
        ftp_close($this->conn);

        if ($file_size > 0) {
            @unlink($src_file);
            return $dst_file;
        } else {
            return '';
        }
    }

    public function video_play($remote_file)
    {
        $size = ftp_size($this->conn, $remote_file);
        if ($size > 0) {
            header("Content-Type: application/octet-stream");
            header("Content-Disposition: attachment; filename=" . basename($remote_file));
            header("Content-Length: $size");
            ftp_get($this->conn, 'php://output', $remote_file, FTP_BINARY);
            ftp_close($this->conn);
        }
    }

    public function remove_file($file)
    {
        //删除远程的旧文件
        if (!empty($file)) {
            if (ftp_size($this->conn, $file) > 0) {
                $res = ftp_delete($this->conn, $file);
                if ($res) {
                    $this->write_log($file . '删除成功');
                } else {
                    $this->write_log($file . '删除失败');
                }
            }
        }

        ftp_close($this->conn);
    }

    public function write_log($str)
    {
        $log_str = "[" . date('Y-m-d H:i:s') . "]" . ": {$str}\r\n";
        if (($fp = fopen($this->src_path . 'upload_ftp_log.txt', 'a+')) !== false) {
            fputs($fp, $log_str);
            fclose($fp);
        }
    }
}

使用curl的操作类

查看代码
 <?php

class Curl
{
    private $uri = 'ftp://username:password@ip:port';
    private $src_path = '../temp/';
    private $root_path = '/path';
    private $remote_path = '';

    public function __construct($subPath)
    {
        $this->remote_path = $this->uri . $this->root_path . '/' . $subPath . '/';
    }

    public function  upload_file($upload_file)
    {
        $local_file = $this->src_path . $upload_file;
        $remote_file = $this->remote_path . $upload_file;
        $file_handle = fopen($local_file, 'r');
        if (!$file_handle) {
            $this->write_log($local_file . '打开失败');
        }
        $file_size = filesize($local_file);

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $remote_file);
        curl_setopt($ch, CURLOPT_FTP_CREATE_MISSING_DIRS, true); // 允许创建缺失的本地目录
        curl_setopt($ch, CURLOPT_UPLOAD, true);
        curl_setopt($ch, CURLOPT_INFILE, $file_handle);
        curl_setopt($ch, CURLOPT_INFILESIZE, $file_size);
        curl_setopt($ch, CURLOPT_FTP_USE_EPRT, true);
        $result = curl_exec($ch);
        if (curl_errno($ch)) {
            $this->write_log($local_file . "上传失败: " . curl_error($ch));
        } else {
            $this->write_log($local_file . "文件上传成功 ");
        }

        fclose($file_handle);
        curl_close($ch);
    }

    public function video_play($file_path)
    {
        $url = $this->uri . $file_path;

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $content = curl_exec($ch);

        if (curl_errno($ch)) {
            $this->write_log($file_path . "读取失败: " . curl_error($ch));
            curl_close($ch);
        } else {
            curl_close($ch);

            $size = strlen($content);
            header("Content-Type: application/octet-stream");
            header("Content-Disposition: attachment; filename=" . basename($file_path));
            header("Content-Length: $size");
            echo $content;
            exit;
        }
    }

    public function remove_file($file_path)
    {
        $url = $this->uri . $file_path;

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POSTQUOTE, array("DELE " . $url)); // DELE命令用于删除文件
        $result = curl_exec($ch);
        curl_close($ch);

        if ($result === FALSE) {
            $this->write_log($file_path . "删除失败: " . curl_error($ch));
        } else {
            $this->write_log($file_path . "删除成功 ");
        }
    }

    public function write_log($str)
    {
        $log_str = "[" . date('Y-m-d H:i:s') . "]" . ": {$str}\r\n";
        if (($fp = fopen($this->src_path . 'upload_ftp_log.txt', 'a+')) !== false) {
            fputs($fp, $log_str);
            fclose($fp);
        }
    }
}

另附curl上传文件的另一种方法

查看代码
 $post_data = [
      "message" => $message,
      'picture' => new CURLFile($pic_path),
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
$output = json_decode(curl_exec($ch), true);
curl_close($ch);

因为工作电脑上找不到大的视频文件,所以用代码合成一个,合成的视频生成mp4格式可以在PC上播放,却不能在浏览器中播放(原因大概是编解码器的问题),转换成webm格式

查看代码
 import cv2


def concat_videos():
    # 待拼接视频文件列表
    video_file = "./media/kinect.mp4"
    video_files = list(map(lambda x: video_file, range(20)))

    # 设置输出视频的分辨率和编码格式
    fourcc = cv2.VideoWriter_fourcc(*"mp4v")
    output_video_path = "output.mp4"
    cap = cv2.VideoCapture(video_files[0])
    fps = cap.get(cv2.CAP_PROP_FPS)
    size = (int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))

    print(fps, size)

    # 创建VideoWriter对象
    out = cv2.VideoWriter(output_video_path, fourcc, fps, size)

    for video_file in video_files:
        cap = cv2.VideoCapture(video_file)

        ret, frame = cap.read()
        while ret:
            # 写入帧到输出视频
            out.write(frame)

            ret, frame = cap.read()

        cap.release()

    # 释放VideoWriter对象
    out.release()


def mp4v_to_h264(filename):
    src_video = filename + ".mp4"
    output_video = filename + "_h264.webm"

    cap = cv2.VideoCapture(src_video)
    fps = cap.get(cv2.CAP_PROP_FPS)
    size = (int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))

    # 创建VideoWriter对象
    fourcc = cv2.VideoWriter_fourcc(*"VP80")
    out = cv2.VideoWriter(output_video, fourcc, 20.0, size)

    # 读取输入视频
    cap = cv2.VideoCapture(src_video)

    while cap.isOpened():
        ret, frame = cap.read()
        if not ret:
            break

        # 写入帧到输出视频文件
        out.write(frame)

    # 释放资源
    cap.release()
    out.release()


concat_videos()
mp4v_to_h264("output")

上传到server的部分mp4文件在Edge浏览器中只能解码音频,firefox浏览器中不支持播放,音频和画面都没有,谷歌chrome浏览器中可以正常播放。

工作电脑上不能安装FFmpeg或者VLC player,只好用python检测下编码

检测视频编码:

查看代码
 from pymediainfo import MediaInfo
import json


def get_video_codec(file_path):
    media_info = MediaInfo.parse(file_path)
    data = media_info.to_json()
    json_data = json.loads(data)
    video_codec = ""
    audio_codec = ""
    for key in json_data:
        for item in json_data[key]:
            for k in item:
                if k == "codecs_video":
                    video_codec = item[k]
                elif k == "audio_codecs":
                    audio_codec = item[k]
    return video_codec, audio_codec


lst = ["flowers.mp4", "M3.MP4", "output3_h264.webm"]
for file_path in lst:
    codec = get_video_codec(file_path)
    print(file_path, codec)

结果,不能正常播放的 M3.MP4 视频编码 HEVC=H.265  

flowers.mp4 ('AVC', 'AAC LC')
M3.MP4 ('HEVC', 'AAC LC')
output3_h264.webm ('VP8', '')

html支持三种视频格式:

  • MPEG4:带有 H.264 视频编码和 AAC 音频编码的 MPEG 4 文件
  • OGG: 带有 Theora 视频编码和 Vorbis 音频编码的 Ogg 文件
  • WebM: 带有 VP8 视频编码和 Vorbis 音频编码的 WebM 文件
posted @ 2024-10-30 16:35  carol2014  阅读(42)  评论(0编辑  收藏  举报