You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在PHP(Laravel)中追加视频流片段

屏幕录制片段合并问题

我首次做视频相关开发,现在遇到个问题:想实现录制用户屏幕,每5秒把片段发去Laravel后端,后端把这些5秒片段合并成完整视频(比如3段合并成15秒),但现在合并后的视频在MacOS上打不开,不知道问题出在哪。

已尝试的代码

前端JS代码

function accessRecord(id) {
    navigator.mediaDevices.getDisplayMedia({
        audio: false,
        video: true
    }).then(startStream).catch(failedStream)
}

function startStream(stream) {
    const mediaRecorder = new MediaRecorder(stream, {
        // 试过添加mimeType
        // mimeType: 'video/mp4'
    });

    mediaRecorder.start(5000);

    mediaRecorder.ondataavailable = (e) => {
        fetch('http://localhost/api/session/12/video/stream', {
            method: 'POST',
            body: new Blob([e.data]),
        }).then(() => {
            console.log('success')
        }).catch((e) => {
            console.log('error')
            console.log(e);
        })
    };
}

Laravel后端代码

Route::post('session/{id}/video/stream', function (Request $request) {
    $videoData = $request->getContent();

    $filename = 'video_uuid-video.mp4';

    if (!file_exists(storage_path('app/videos/' . $filename))) {
        touch(storage_path('app/videos/' . $filename));
    }

    \Illuminate\Support\Facades\Storage::append('videos/' . $filename, $videoData);

    return response()->json(['message' => 'Video frame uploaded successfully']);
});

问题根源

直接把视频片段的二进制内容追加写入文件是行不通的。像MP4、WebM这类视频文件有特定的容器结构,包含文件头、索引信息、媒体数据等部分,单个录制片段没有完整的文件头,直接拼接会破坏文件结构,导致播放器无法识别。另外,MediaRecorder默认生成的是WebM格式,强制命名为.mp4本身就不符合格式规范。

解决方案步骤

1. 前端统一录制格式

先检测浏览器支持的标准视频格式,保证片段兼容性:

// 优先选择浏览器支持的格式
const supportedMimeTypes = [
    'video/webm;codecs=vp9,opus',
    'video/webm;codecs=vp8,opus',
    'video/mp4;codecs=avc1,mp4a'
];
const mimeType = supportedMimeTypes.find(type => MediaRecorder.isTypeSupported(type));

const mediaRecorder = new MediaRecorder(stream, { mimeType });

2. 后端单独存储片段,用FFmpeg合并

视频合并必须依赖专业多媒体工具FFmpeg,它能正确处理视频容器结构,把片段合并为可播放的完整视频。

修改后端存储片段的逻辑

use Illuminate\Support\Str;

Route::post('session/{id}/video/stream', function (Request $request) {
    $sessionId = $id;
    // 生成唯一片段文件名,按会话分组存储
    $segmentName = 'segments/' . $sessionId . '_' . Str::uuid() . '.webm';
    // 单独存储每个片段
    \Illuminate\Support\Facades\Storage::put($segmentName, $request->getContent());

    return response()->json(['message' => 'Video segment uploaded successfully']);
});

添加合并接口,调用FFmpeg处理

Route::post('session/{id}/video/merge', function (Request $request) {
    $sessionId = $id;
    $segmentDir = storage_path('app/segments/');
    // 获取当前会话下的所有片段,按创建时间排序
    $segments = glob($segmentDir . $sessionId . '_*.webm');
    usort($segments, function($a, $b) {
        return filemtime($a) - filemtime($b);
    });

    // 生成FFmpeg需要的片段列表文件
    $listFile = $segmentDir . $sessionId . '_list.txt';
    $listContent = '';
    foreach ($segments as $seg) {
        $listContent .= "file '" . str_replace("'", "\\'", $seg) . "'\n";
    }
    file_put_contents($listFile, $listContent);

    // 生成合并后的完整视频
    $outputFile = storage_path('app/videos/' . $sessionId . '_full.webm');
    // 执行FFmpeg合并命令,直接复制编码保证效率
    exec("ffmpeg -f concat -safe 0 -i $listFile -c copy $outputFile");

    // 清理临时文件(可选)
    unlink($listFile);
    foreach ($segments as $seg) {
        unlink($seg);
    }

    return response()->json(['message' => 'Video merged successfully', 'video_path' => $outputFile]);
});

3. 前端发送录制结束信号

停止录制时调用合并接口:

function stopRecording() {
    mediaRecorder.stop();
    // 触发后端合并逻辑
    fetch(`http://localhost/api/session/12/video/merge`, {
        method: 'POST'
    }).then(res => res.json())
      .then(data => console.log(data.message));
}

关键注意事项

  • 服务器必须安装FFmpeg:Linux用apt install ffmpeg,Mac用brew install ffmpeg。
  • 提前创建storage/app/segments目录并确保读写权限。
  • 如果需要输出MP4格式,修改FFmpeg命令为:
    ffmpeg -f concat -safe 0 -i $listFile -c:v libx264 -c:a aac $outputFile.mp4
    

内容的提问来源于stack exchange,提问作者Bruno Francisco

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 00:07:45