ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

视频字幕如何烧录到成片?硬字幕、软字幕、样式控制与播放验收

视频字幕如何烧录到成片?硬字幕、软字幕、样式控制与播放验收 一份已经确认的 SRT不等于用户能看到正确字幕。有人把字幕文件放在视频旁边以为播放器会自动加载有人把字幕硬烧进视频却在部署机上缺少中文字体还有人只检查 FFmpeg 退出码没有发现成片时长变短或字幕根本没有出现。本文解决的是“确认字幕怎样成为可交付视频”的工程问题。固定案例是一条 93 秒、1920×1080 的中文讲解视频已有确认版confirmed.srt。网站需要可开关、可切换语言的字幕轨短视频平台需要字幕直接显示在画面中。系统必须据此选择软字幕或硬字幕并留下可复核的输出记录。示例环境为 Python 3 Worker 执行 FFmpegJava 17/Spring Boot 风格服务层管理任务和产物MySQL 8.x 保存渲染记录。命令参数、字体文件和路径均为教学示例应按实际环境配置。目录先判断该用硬字幕还是软字幕固定案例和交付目标字幕烧录的处理链路数据模型一次渲染必须可追溯Python实现生成安全的FFmpeg任务Java实现只登记验收通过的成片预期输出和自动测试SQL验证上线后怎样核对字幕成片异常边界和交付验收小结和延伸阅读一、先判断该用硬字幕还是软字幕“加字幕”至少有两种交付方式。软字幕把字幕轨封装进容器播放器负责显示硬字幕在渲染时写进每一帧画面。它们不是谁更高级而是交付约束不同交付场景推荐方式原因限制自有网站、课程播放器软字幕用户可开关、换语言、调字号播放器必须支持字幕轨和 UTF-8短视频平台、社交媒体硬字幕上传后不依赖外部 SRT 文件字体、换行和安全边距固定在画面中留存母版软字幕加原始 SRT保留可编辑文本和多语言空间不能把母版误当最终发布文件审核预览硬字幕预览版审核人打开即可核对画面预览版不替代字幕资产固定案例的网页版本输出lesson-soft.mp4内含中文字幕轨平台版本输出lesson-burned.mp4字幕已写入画面。两份文件都来自同一个CONFIRMED_SRT不能各自手工修改。图1软字幕保留播放端控制能力硬字幕换取跨平台的确定展示。二、固定案例和交付目标任务编号VW-20260930-005 源视频SOURCE_VIDEO / source/original.mp4 确认字幕CONFIRMED_SRT / outputs/subtitle/confirmed.srt 视频参数1920x108093.2 秒25 fps 硬字幕输出outputs/delivery/lesson-burned.mp4 软字幕输出outputs/delivery/lesson-soft.mp4 字体Noto Sans CJK SC字号 42底边距 72 像素开发机上的预览曾正常部署机却缺少同一套中文字体FFmpeg 返回成功但部分字符显示为方框。另一条任务把时间轴单位理解错导出文件只剩一小段。结论是字幕是否出现、文字是否能显示、时长是否接近源视频都要成为交付条件。三、字幕烧录的处理链路无论硬字幕还是软字幕都不应该直接覆盖最终文件。正确链路是读取确认输入、生成临时输出、探测媒体、检查交付规则、最后原子登记步骤硬字幕处理软字幕处理关键证据读取输入视频与确认版 SRT 均为READY同左文件摘要、版本号选择配置字体、字号、边距、颜色语言、标题、默认轨配置版本FFmpeg 生成subtitles滤镜写入画面复制视频流并封装字幕轨临时文件、stderr 摘要媒体探测检查时长、分辨率、视频流额外检查 subtitle streamffprobe 结果提交产物验收通过后移动为最终文件验收通过后移动为最终文件sha256、大小、状态图2两种成片共享同一份确认字幕但使用不同的渲染和验收规则。硬字幕的“字幕是否可见”不能只靠ffprobe判断因为字幕已变成像素。生产中可保存实际字体与滤镜参数、抽取指定时间点预览帧供人工核对、并将原始字幕与渲染任务关联。软字幕则能通过媒体流检查语言与编码。四、数据模型一次渲染必须可追溯不要只在任务表里留一个成片路径。至少记录该成片使用了哪一版字幕、哪套样式和哪种交付方式CREATETABLEsubtitle_render_job(idBIGINTPRIMARYKEYAUTO_INCREMENT,job_noVARCHAR(64)NOTNULL,source_video_idBIGINTNOTNULL,subtitle_version_idBIGINTNOTNULL,delivery_modeVARCHAR(16)NOTNULL,render_statusVARCHAR(24)NOTNULL,style_profileVARCHAR(64)NULL,font_nameVARCHAR(128)NULL,language_codeVARCHAR(16)NULL,output_pathVARCHAR(500)NULL,output_sha256CHAR(64)NULL,output_duration_msBIGINTNULL,output_widthINTNULL,output_heightINTNULL,error_codeVARCHAR(64)NULL,error_messageVARCHAR(1000)NULL,request_idVARCHAR(64)NOTNULL,create_timeDATETIMENOTNULL,update_timeDATETIMENULL,UNIQUEKEYuk_render_request(request_id),KEYidx_render_video(source_video_id,render_status),CHECK(delivery_modeIN(BURNED,SOFT)),CHECK(render_statusIN(PENDING,RUNNING,READY,FAILED)));同一份确认字幕可以生成多个交付物但每个交付物有独立渲染记录。READY只表示文件已通过当前交付规则的验收不表示以后不能因样式变更重新生成新版本。图3渲染记录回答“这份成片用了什么字幕、什么样式为什么可以交付”。五、Python实现生成安全的FFmpeg任务Worker 不接受前端直接拼接的 FFmpeg 字符串而是接收结构化请求检查字幕版本、字体和样式配置后构造参数。下面是硬字幕任务的核心部分fromdataclassesimportdataclassfrompathlibimportPathimportsubprocessdataclass(frozenTrue)classBurnRequest:video:Path subtitle:Path output:Path font_name:strNoto Sans CJK SCfont_size:int42margin_v:int72defbuild_burn_command(request:BurnRequest)-list[str]:ifnotrequest.video.is_file()ornotrequest.subtitle.is_file():raiseValueError(源视频或确认字幕不存在)ifrequest.font_size20orrequest.font_size96:raiseValueError(字幕字号不在允许范围)subtitle_pathrequest.subtitle.as_posix().replace(,r\\)stylefFontName{request.font_name},FontSize{request.font_size},MarginV{request.margin_v}filter_argfsubtitlesfilename{subtitle_path}:force_style{style}temporaryrequest.output.with_suffix(request.output.suffix.writing)return[ffmpeg,-y,-i,str(request.video),-vf,filter_arg,-c:v,libx264,-crf,20,-preset,medium,-c:a,aac,-movflags,faststart,str(temporary)]defrender_burned(request:BurnRequest)-Path:completedsubprocess.run(build_burn_command(request),capture_outputTrue,textTrue,timeout900)ifcompleted.returncode!0:raiseRuntimeError(completed.stderr[-1200:])returnrequest.output.with_suffix(request.output.suffix.writing)软字幕不需要视频滤镜通常复制视频和音频流并把字幕封装到 MP4ffmpeg -i source.mp4 -i confirmed.srt \ -map 0:v -map 0:a? -map 1:0 \ -c:v copy -c:a copy -c:s mov_text \ -metadata:s:s:0 languagechi -metadata:s:s:0 title中文 \ lesson-soft.mp4.writing.writing很关键没有完成探测和验收的文件不能使用正式扩展名更不能登记为READY。图4外部命令成功只是中间结果媒体探测和文件提交共同决定成片是否可用。六、Java实现只登记验收通过的成片Java 服务层锁定请求、读取已确认字幕调用 Worker 后核验结构化媒体信息Transactional(rollbackForException.class)publicRenderResultrender(RenderCommandcommand){SubtitleRenderJobjobrenderRepository.lockByRequestId(command.requestId()).orElseGet(()-renderRepository.createPending(command));if(READY.equals(job.status()))returnRenderResult.reused(job.outputPath());WorkflowFilevideofileRepository.findReady(command.videoId(),SOURCE_VIDEO).orElseThrow(()-newBizException(源视频不可用));SubtitleVersionsubtitlesubtitleRepository.findConfirmed(command.subtitleVersionId()).orElseThrow(()-newBizException(字幕尚未确认不能渲染));renderRepository.markRunning(job.id());WorkerRenderResultoutputrenderWorker.render(video.path(),subtitle.path(),command.profile());if(!output.success()||!output.mediaInfo().isPlayable()){renderRepository.markFailed(job.id(),output.errorCode(),output.errorMessage());returnRenderResult.failed(output.errorCode());}if(Math.abs(output.mediaInfo().durationMs()-video.durationMs())1500){renderRepository.markFailed(job.id(),DURATION_MISMATCH,成片时长偏差超过 1.5 秒);returnRenderResult.failed(DURATION_MISMATCH);}StringoutputPathfileRepository.commitTemporary(output.temporaryPath(),command.deliveryRole());renderRepository.markReady(job.id(),outputPath,output.sha256(),output.mediaInfo());returnRenderResult.ready(outputPath);}“FFmpeg 返回 0”和“成片可交付”是两件事。后者还要满足时长、分辨率、文件大小、软字幕流或硬字幕预览等规则。七、预期输出和自动测试硬字幕lesson-burned.mp41920x1080时长约 93.2 秒字幕在底部安全区域可见 软字幕lesson-soft.mp4包含 1 条 chi / mov_text 字幕轨可由播放器开关 两份成片均关联 CONFIRMED-V3不关联草稿或提议字幕 失败任务保留错误摘要和临时文件清理记录不生成 READY 产物deftest_burn_command_rejects_missing_subtitle(tmp_path):requestBurnRequest(tmp_path/source.mp4,tmp_path/missing.srt,tmp_path/out.mp4)withpytest.raises(ValueError,match确认字幕不存在):build_burn_command(request)deftest_burn_command_writes_to_temporary_file(sample_video,sample_srt,tmp_path):commandbuild_burn_command(BurnRequest(sample_video,sample_srt,tmp_path/lesson.mp4))assertcommand[-1].endswith(lesson.mp4.writing)TestvoidshouldRejectUnconfirmedSubtitle(){fixture.readyVideo(100L,93_200L);fixture.subtitleVersion(200L,WAITING_CONFIRM);BizExceptionerrorassertThrows(BizException.class,()-service.render(newRenderCommand(REQ-005,100L,200L,BURNED)));assertTrue(error.getMessage().contains(字幕尚未确认));}TestvoidshouldFailWhenDurationDriftsTooFar(){fixture.readyVideo(100L,93_200L);fixture.confirmedSubtitle(200L);worker.stubSuccess(90_000L,1920,1080);RenderResultresultservice.render(newRenderCommand(REQ-006,100L,200L,BURNED));assertEquals(DURATION_MISMATCH,result.errorCode());}八、SQL验证上线后怎样核对字幕成片-- 已交付但时长明显偏离源视频的任务预期结果为空SELECTr.job_no,r.delivery_mode,r.output_duration_ms,v.duration_msASsource_duration_msFROMsubtitle_render_job rJOINworkflow_file vONv.idr.source_video_idWHEREr.render_statusREADYANDABS(r.output_duration_ms-v.duration_ms)1500;-- 软字幕成片应带有语言信息预期结果为空SELECTjob_no,output_pathFROMsubtitle_render_jobWHEREdelivery_modeSOFTANDrender_statusREADYAND(language_codeISNULLORlanguage_code);-- 同一请求只允许一条渲染记录预期结果为空SELECTrequest_id,COUNT(*)AScntFROMsubtitle_render_jobGROUPBYrequest_idHAVINGCOUNT(*)1;图5没有通过媒体与交付规则核验的文件只能保留为失败证据不能被下游当作成片。九、异常边界和交付验收异常应对方式缺少中文字体预检字体文件或字体族缺失时阻断硬字幕渲染SRT 编码错误入库时统一 UTF-8解析失败不进入确认版本字幕被画面裁切使用分辨率对应的安全边距保留预览帧抽检FFmpeg 超时或异常退出保存 stderr 摘要清理临时文件允许重新发起成片时长或分辨率异常标记失败不移动到正式交付目录同一请求重复提交以request_id复用结果或阻止并发执行上线验收至少应完成硬字幕在目标分辨率预览帧可读软字幕在目标播放器可开关成片时长、分辨率和文件大小符合规则每份成片可追溯到确认字幕版本与样式配置失败记录能定位字体、编码、命令或媒体探测问题。十、小结和延伸阅读字幕烧录的关键不是把一条 FFmpeg 命令跑起来而是根据交付场景选择硬字幕或软字幕让确认版字幕、样式配置、临时文件、媒体探测和最终成片形成一条可验收的链路。这样网页播放器保留字幕轨短视频平台也能稳定展示文字而失败文件不会混进正式交付物。FFmpeg Filters DocumentationFFprobe DocumentationSpring Framework: Transaction ManagementMySQL 8.0 Reference Manual: CREATE TABLE
返回列表