使用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 文件