18 KiB
API-02 媒体处理(MEDIA)
共 9 个接口:MEDIA-01、MEDIA-02、MEDIA-02A~02E、MEDIA-03、MEDIA-04
存储后端:本项目媒体存储与转码已全量托管给外部 SaaS「老司机媒体库」,后端自身不做加密/转码/CDN。
- 图片:同步上传完成 → 直接返回可用 URL
- 视频:上传完成后返回
mediaId + processing→ 后端后台转发到 SaaS 并轮询转码结果 → 就绪后可播放
状态枚举映射
media.process_status(处理状态):
| DB值 | API响应值 | 说明 |
|---|---|---|
| 1 | processing | 处理中(视频上传完成,等待转码;图片不会停留在此状态) |
| 2 | completed | 处理完成 |
| 3 | failed | 处理失败(转码失败/超时/队列繁忙) |
图片为同步流程,成功即
completed;视频为异步流程,典型耗时 5–60 秒。客户端不要把视频上传接口返回视为可播放完成态,必须以 MEDIA-04 的status=completed为准。
媒体 URL 说明(重要)
| 媒体类型 | 后端返回形态 | 前端处理 |
|---|---|---|
| 图片类 URL | 老司机/CDN 绝对地址。常见为 {img_cn_cdn}/xxx.bnc,也可能是历史兼容绝对地址 |
后端不会直接返回 /api/media-bnc?url=...。H5 渲染前必须先经过同源代理 helper 转换;iOS/Android 按各端现有媒体加载策略处理 |
| 视频播放 URL | https://{api_host}/client/api/v1/media/:mediaId/main.m3u8?token=... |
直接交给 HLS 播放器。后端会再向 LSJ 拉取 m3u8 原文;不需要额外带 Authorization header |
| 视频短预览 URL | https://{api_host}/client/api/v1/media/:mediaId/preview.m3u8?token=... |
用 <video autoplay muted loop playsinline> 渲染,不是静态图;后端会再向 LSJ 拉取预览 m3u8 |
后端当前不会把所有图片字段改写成某个前端站点自己的
/api/media-bnc?url=...。 H5 侧应在渲染前,把图片类 URL 通过前端 helper 映射为同源代理地址;不要把接口返回的 LSJ 图片 URL 直接塞进<img src>、<video poster>、canvasnew Image()或 CSSbackground-image。 头像、Banner、帖子封面、评论图片、PPV 图片等都属于“图片类 URL”,处理规则一致。 视频类字段不会再直接暴露movie_cn_cdn/movie_vip_cdn,而是统一返回我方 API 域名下的 m3u8 中转地址;后端为保证签名有效性,视频播放 token 与上游 LSJ 签名都不做落库,每次返回时动态签发。
MEDIA-01 上传图片
| 项目 | 说明 |
|---|---|
| 接口路径 | POST /client/api/v1/media/image |
| 接口用途 | 上传图片到老司机媒体库 |
| Content-Type | multipart/form-data |
| 权限要求 | 需登录 |
| 涉及表 | media |
接口逻辑:
- 校验文件扩展名与 MIME(JPG/PNG/GIF/WEBP)和大小上限(普通图片 ≤10MB,头像/Banner ≤5MB)
- 流式上传到老司机
/upload/image - 写入
media表:type=1、process_status=2(同步就绪)、storage_path = 老司机返回的 file、metadata = {provider:"lsj", provider_id, file} - 返回
img_cn_cdn + .bnc的可访问 URL
请求参数(FormData):
| 字段 | 类型 | 必选 | 说明 |
|---|---|---|---|
| file | File | 是 | 图片文件,支持 JPG/PNG/GIF/WEBP;普通图片最大10MB,头像/Banner 最大5MB |
| type | string | 是 | 用途类型:avatar/banner/post/ppv/id_card(映射 usage_type 2/3/1/4/5) |
响应数据:
| 字段 | 类型 | 说明 |
|---|---|---|
| mediaId | string | 媒体ID |
| url | url | 图片 URL(后端返回老司机/CDN 绝对地址;H5 渲染前需先转同源代理) |
| thumbnailUrl | url | 缩略图 / 小图 URL(若上游未返回则可能为空;H5 渲染规则同 url) |
| blurUrl | url | 模糊图 / 小图 URL(若上游未返回则可能为空;H5 渲染规则同 url) |
| mimeType | string | 文件MIME类型 |
| size | number | 文件大小(字节) |
| width | number | 图片宽度(像素,目前为 0,老司机不回传) |
| height | number | 图片高度(像素,目前为 0) |
| status | string | 固定为 completed |
MEDIA-02 上传视频
| 项目 | 说明 |
|---|---|
| 接口路径 | POST /client/api/v1/media/video |
| 接口用途 | 上传视频到老司机媒体库 |
| Content-Type | multipart/form-data |
| 权限要求 | 需登录 |
| 涉及表 | media |
接口逻辑:
- 校验扩展名、MIME(MP4/MOV/WebM;包含 iOS QuickTime/MOV)、大小(≤1GB)
- 后端按服务端配置分片调老司机
/upload/byte,最后一片拿到file/id。当前默认 1MB,上限 8MB;此前测试环境 4MB 灰度已回退 - 写入
media表:type=2、process_status=1、storage_path = 老司机返回的 file (原始 mp4 路径)、metadata = {provider:"lsj", provider_id} - 推入后台转码轮询队列(内存 channel,4 worker、3s 间隔、10 分钟超时);入队阻塞最多 5 秒,仍失败则直接写
process_status=3并返回50001 视频处理队列繁忙,请稍后重试 - 立即返回
status=processing+mediaId
转码回写:后台 worker 调老司机
/upload/query拿到file_status=2后,把storage_path更新为file_m3u8(真正可播放路径),thumbnail_url写入file_preview_m3u8(短预览 m3u8 原始路径),并回填width/height/duration。
请求参数(FormData):
| 字段 | 类型 | 必选 | 说明 |
|---|---|---|---|
| file | File | 是 | 视频文件,支持 MP4/MOV/WebM(包含 iOS QuickTime/MOV),最大1GB |
| type | string | 是 | 用途类型:post/ppv(映射 usage_type 1/4)。视频不支持 id_card |
响应数据:
| 字段 | 类型 | 说明 |
|---|---|---|
| mediaId | string | 媒体ID |
| mimeType | string | 文件MIME类型 |
| size | number | 文件大小(字节) |
| status | string | 固定为 processing(异步转码中) |
上传响应仍只返回
mediaId + processing。coverUrl/blurPreviewUrl/signedUrl/width/height/duration这类完成态字段,需要在转码完成后通过 MEDIA-04 或 MEDIA-03 获取。
MEDIA-02A 初始化客户端分片上传
| 项目 | 说明 |
|---|---|
| 接口路径 | POST /client/api/v1/media/upload/init |
| 接口用途 | 初始化一次客户端分片上传会话,返回 uploadId |
| Content-Type | application/json |
| 权限要求 | 需登录 |
| 涉及表 | 无(会话保存在服务端内存态 ChunkedUploadStore) |
适用场景:
- 大视频走客户端分片上传,不再使用
multipart/form-data直接调MEDIA-02 - 图片也可走这套协议,但当前 H5 常规图片仍主要使用
MEDIA-01
接口逻辑:
- 校验
kind、filename、size、chunkSize - 服务端创建一次分片上传会话,生成
uploadId - 返回服务端确认后的
chunkSize、totalChunks
请求参数(JSON):
| 字段 | 类型 | 必选 | 说明 |
|---|---|---|---|
| filename | string | 是 | 原始文件名 |
| size | number | 是 | 文件总大小(字节) |
| chunkSize | number | 是 | 每片大小(字节) |
| kind | string | 是 | image / video |
| type | string | 是 | 用途类型:post / ppv / avatar / banner / id_card 等 |
| mimeType | string | 否 | MIME 类型;未传时由后端按文件名和后续处理兜底识别 |
响应数据:
| 字段 | 类型 | 说明 |
|---|---|---|
| uploadId | string | 分片上传会话 ID |
| chunkSize | number | 服务端接受的分片大小 |
| totalChunks | number | 总分片数 |
| received | number[] | 已接收的分片索引,初始化时通常为空 |
服务端对分片上传会话数量有限流;同一用户同时进行中的会话过多时会返回业务错误。
MEDIA-02B 上传单个分片
| 项目 | 说明 |
|---|---|
| 接口路径 | PUT /client/api/v1/media/upload/:uploadId/chunk/:idx |
| 接口用途 | 上传指定索引的单个分片 |
| Content-Type | application/octet-stream |
| 权限要求 | 需登录 |
| 涉及表 | 无 |
接口逻辑:
- 按
uploadId找到服务端分片会话,并校验归属用户 - 校验
idx是否越界 - 校验当前分片大小:
- 非最后一片必须等于
chunkSize - 最后一片必须等于剩余大小
- 非最后一片必须等于
- 把分片写入服务端临时目录;已上传过的相同分片按幂等成功返回
- 返回当前已接收分片数
路径参数:
| 字段 | 类型 | 必选 | 说明 |
|---|---|---|---|
| uploadId | string | 是 | 分片上传会话 ID |
| idx | number | 是 | 当前分片索引,从 0 开始 |
请求体:
| 字段 | 类型 | 必选 | 说明 |
|---|---|---|---|
| body | binary | 是 | 当前分片的二进制内容 |
请求头:
| 字段 | 必选 | 说明 |
|---|---|---|
| Content-Type | 是 | 固定 application/octet-stream |
| Content-Length | 建议 | 当前分片大小;服务端会据此和期望分片大小进行校验 |
| Content-MD5 | 否 | 可选;传入时服务端会校验分片 MD5 |
响应数据:
| 字段 | 类型 | 说明 |
|---|---|---|
| uploadId | string | 分片上传会话 ID |
| index | number | 当前分片索引 |
| received | number | 当前已接收分片数 |
| total | number | 总分片数 |
| allChunked | boolean | 是否已收齐全部分片 |
若某个分片请求没有到达应用层,
status/complete阶段会表现为对应索引仍在missing列表中。
MEDIA-02C 查询分片上传状态
| 项目 | 说明 |
|---|---|
| 接口路径 | GET /client/api/v1/media/upload/:uploadId |
| 接口用途 | 查询某次分片上传会话已收到/缺失的分片 |
| 权限要求 | 需登录 |
| 涉及表 | 无 |
路径参数:
| 字段 | 类型 | 必选 | 说明 |
|---|---|---|---|
| uploadId | string | 是 | 分片上传会话 ID |
响应数据:
| 字段 | 类型 | 说明 |
|---|---|---|
| uploadId | string | 分片上传会话 ID |
| totalChunks | number | 总分片数 |
| received | number[] | 已成功接收的分片索引 |
| missing | number[] | 当前仍缺失的分片索引 |
典型用途:
- 断点续传
complete失败后排查缺失分片- 客户端决定是否重传
missing中的分片
MEDIA-02D 完成客户端分片上传
| 项目 | 说明 |
|---|---|
| 接口路径 | POST /client/api/v1/media/upload/:uploadId/complete |
| 接口用途 | 在全部分片到齐后合并文件并生成媒体记录 |
| 权限要求 | 需登录 |
| 涉及表 | media |
接口逻辑:
- 校验上传会话存在且归属当前用户
- 若仍有缺失分片,返回
40901,并拒绝进入合并流程 - 合并所有分片为完整文件,并根据真实文件内容嗅探 MIME
- 根据
kind调用后续处理:image:同步上传到老司机图片接口并返回完成态video:先写media记录并返回processing,后台继续执行上传老司机 -> 转码轮询 -> 更新 completed
- 成功或失败后清理上传会话
视频分片完成语义:
complete在服务端确认分片完整、合并成功并生成mediaId后即可返回,不再同步等待老司机上传和转码。- 视频初始写入
media.process_status=processing,storage_path为后端占位路径;后台 LSJ 上传成功后会更新为老司机原始视频路径,转码完成后再更新为可播放 m3u8。 - 后台任务使用内存队列;服务重启期间未完成任务不会自动恢复,相关媒体可能停留在
processing,需后续持久化任务表版本解决。 - LSJ 上传失败、后台队列失败或转码失败时,
media.process_status会更新为failed,失败原因通过 MEDIA-04 的errorMessage返回。 - 仅分片视频异步链路在失败终态时会写一条系统站内通知:
category=system、type=system、relatedType=media、relatedId=mediaId,标题为“视频上传失败”。 - 视频真正可播放并进入待审核状态后,后端会写一条系统站内通知:
category=system、type=publish_success、relatedType=media、relatedId=mediaId,标题为“视频等待审核”。
路径参数:
| 字段 | 类型 | 必选 | 说明 |
|---|---|---|---|
| uploadId | string | 是 | 分片上传会话 ID |
响应数据:
| 字段 | 类型 | 说明 |
|---|---|---|
| mediaId | string | 媒体ID |
| mimeType | string | 文件MIME类型 |
| size | number | 文件大小(字节) |
| width | number | 图片/视频宽度;视频通常在转码完成后才完整回填 |
| height | number | 图片/视频高度 |
| status | string | 图片为 completed,视频通常为 processing |
40901的含义是:当前uploadId仍有未上传成功的分片,最常见表现是missing=[0]或其他缺失索引。视频场景下,
complete返回mediaId只代表“客户端分片已被服务端完整接收并进入后台处理”,不代表视频已经可播放。前端仍需轮询 MEDIA-04;通知中心里的“视频等待审核 / 视频上传失败”系统通知只能作为辅助提醒,不能代替状态查询。
MEDIA-02E 取消客户端分片上传
| 项目 | 说明 |
|---|---|
| 接口路径 | DELETE /client/api/v1/media/upload/:uploadId |
| 接口用途 | 主动终止一次分片上传会话并清理临时文件 |
| 权限要求 | 需登录 |
| 涉及表 | 无 |
路径参数:
| 字段 | 类型 | 必选 | 说明 |
|---|---|---|---|
| uploadId | string | 是 | 分片上传会话 ID |
接口逻辑:
- 校验上传会话存在且归属当前用户
- 删除服务端临时分片文件
- 清理会话
当前服务端会话是内存态,超时也会被后台 GC 清理;客户端在用户取消上传时仍建议主动调用本接口。
MEDIA-03 获取签名URL
| 项目 | 说明 |
|---|---|
| 接口路径 | GET /client/api/v1/media/:mediaId/signed-url |
| 接口用途 | 获取媒体文件的对外访问地址 |
| 权限要求 | 需登录;是否返回真实地址由媒体所属内容的访问权限决定 |
| 涉及表 | media, post_media, post, user_subscription, ppv_unlock, content_unlock |
接口逻辑:
- 查询
media获取storage_path、type、process_status - 若
process_status != 2→ 返回40901 media 仍在处理中 - 判定当前用户是否有访问权限:
- 自己上传的媒体,直接允许
- 命中 IM 侧
ppv_unlock,允许 - 媒体关联到内容时:
free:允许subscriber:校验有效订阅且tier_level >= requiredTierLevelppv:校验content_unlock
- 根据
type生成 URL:- 图片:
img_cn_cdn + replaceExt(storage_path, ".bnc") - 视频:生成我方 API 域名下的 m3u8 中转地址:
- 主播放流:
/client/api/v1/media/:mediaId/main.m3u8?token=... - 预览播放流:
/client/api/v1/media/:mediaId/preview.m3u8?token=... - 后端收到请求后,再用
movie_cn_cdn + m3u8Path + sign/time/rand/uid向 LSJ 拉取 m3u8 原文并转发
- 主播放流:
- 图片:
- 返回结果;无权限时不返回
signedUrl,仅返回hasAccess=false及预览字段
路径参数:
| 字段 | 类型 | 必选 | 说明 |
|---|---|---|---|
| mediaId | string | 是 | 媒体ID |
响应数据:
| 字段 | 类型 | 说明 |
|---|---|---|
| mediaId | string | 媒体ID |
| signedUrl | url | 图片 .bnc URL / 视频主播放 m3u8 中转地址 |
| coverUrl | url | 视频预览 m3u8 中转地址(视频时返回;图片为空) |
| blurPreviewUrl | url | 与 coverUrl 含义一致,兼容客户端视频预览字段命名 |
| mimeType | string | 文件MIME类型 |
| size | number | 文件大小(字节) |
| expiresAt | timestamp | 视频播放 token 过期时间戳;图片字段为 0 |
| hasAccess | boolean | 当前用户是否有访问权限 |
| width | number | 媒体宽度(像素) |
| height | number | 媒体高度(像素) |
| duration | number | 视频时长(秒,图片为 0) |
MEDIA-04 查询媒体处理状态
| 项目 | 说明 |
|---|---|
| 接口路径 | GET /client/api/v1/media/:mediaId/status |
| 接口用途 | 查询视频上传后的转码进度 |
| 权限要求 | 需登录(只能查自己上传的) |
| 涉及表 | media |
接口逻辑:
- 查询
media,校验user_id == 当前用户,否则40301 - 返回
process_status枚举名 - 若
process_status=3,返回process_error
路径参数:
| 字段 | 类型 | 必选 | 说明 |
|---|---|---|---|
| mediaId | string | 是 | 媒体ID |
响应数据:
| 字段 | 类型 | 说明 |
|---|---|---|
| mediaId | string | 媒体ID |
| status | string | 处理状态:processing / completed / failed |
| progress | number | 仅有 0(处理中/失败)或 100(完成)两态。老司机 /upload/query 不返回百分比,暂不支持真实进度 |
| signedUrl | url | 转码完成后返回视频主播放 m3u8 中转地址 |
| coverUrl | url | 转码完成后返回视频预览 m3u8 中转地址 |
| blurPreviewUrl | url | 与 coverUrl 含义一致,兼容客户端视频预览字段命名 |
| mimeType | string | 文件MIME类型 |
| size | number | 文件大小(字节) |
| width | number | 视频宽度(像素) |
| height | number | 视频高度(像素) |
| duration | number | 视频时长(秒) |
| errorMessage | string | 失败原因(status=failed时返回) |
前端建议:视频上传后以 3–5 秒间隔轮询本接口;拿到
status=completed后,可直接使用本接口返回的signedUrl/coverUrl做回显,或再调用 MEDIA-03 取新的播放地址。不要依赖progress做进度条。分片视频在完成转码或进入失败终态后还会进入通知中心系统分类,客户端可用该通知触发刷新,但不应只依赖通知完成业务闭环。