黑白梦黑白梦

  • 文章
  • 专栏
  • 文章
  • 专栏
全部文章

阿里云视频点播集成实践指南

发布于 2026-10-11约 32 分钟

基于阿里云视频点播与 video.js 构建流媒体处理与播放链路,包含浏览器切片直传与状态轮询、服务端签名与资源防越权核验、多档 HLS 自适应播放与动态换签改写,以及废弃媒资清理与 HLS 标准加密演进路径。

方案架构与核心组件

集成分成五层。路由只解析请求并调用服务;服务做门禁、归属核对和清理编排;适配层只访问阿里云 OpenAPI;浏览器负责直传和播放。控制台配置不进代码。

  • 控制台:配置区域、加速域名、URL 鉴权、Referer 白名单、跨域响应头、转码模板组与 RAM 权限。
  • 适配层:封装创建上传、刷新上传、查询视频、获取播放信息、删除视频等 OpenAPI 调用。调用时动态实例化,模块加载阶段不依赖密钥。
  • 服务层:处理鉴权门禁、归并转码状态、按记录签发播放地址、保存前核对归属以及清理孤儿视频。
  • 路由:对外提供五个 POST 接口,其余方法返回 405。未配置、处理中与不可用通过 HTTP 200 返回,参数与权限错误返回 4xx / 502。
  • 浏览器:使用 aliyun-vod-upload-sdk 直传视频文件;使用 video.js 8 播放自适应 HLS 并维持行为打点。

视频记录保存在已有实体的 JSON 列中,不为点播单独建表:

字段 含义
videoId 点播视频标识。空记录不写该字段
fileName 原始文件名,用于展示,最长 255 字符
durationSeconds 云端就绪后四舍五入得到的视频时长秒数

一次完整的上传至播放数据流向:

  1. 管理端选择文件,向服务端申请上传凭证。服务端调用 CreateUploadVideo,并注入资源归属标签 res-${resourceId}。
  2. 上传 SDK 将文件切片直传至云端存储地址。凭证过期时调用 RefreshUploadVideo 刷新。
  3. 上传完成后,客户端每 5 秒轮询一次 POST /api/aliyun-vod/video-status,服务端转调 GetVideoInfo,直到视频就绪或转码失败(最长轮询 30 分钟)。
  4. 批量保存时,服务端执行差异对比与标签核验,确认新视频归属于当前资源后写入记录。
  5. 拥有观看权限的用户打开播放器,服务端执行履约与准入校验后调用 GetPlayInfo 返回各档 m3u8 地址。播放器动态拼装总列表播放,并在地址过期前静默换签。

接口定义:

路径 作用
POST /api/aliyun-vod/upload-auth 签发上传凭证
POST /api/aliyun-vod/refresh-upload-auth 刷新上传凭证
POST /api/aliyun-vod/video-status 查询并归并转码状态
POST /api/aliyun-vod/play-url 按记录签发播放地址
POST /api/aliyun-vod/delete-video 删除尚未入库的视频

官方接口说明:CreateUploadVideo、RefreshUploadVideo、GetVideoInfo、GetPlayInfo、DeleteVideo。

控制台与运行环境

区域使用华东 2(上海,cn-shanghai)。点播加速域名与站点主域名保持独立(如 vod.example.com),并配置 HTTPS 证书,防止被浏览器当作混合内容拦截。

URL 鉴权使用方式 A,默认有效时长 10 分钟。GetPlayInfo 不传 AuthTimeout,播放地址有效期遵循控制台配置。方式 A 查询串形如 auth_key=timestamp-rand-uid-md5hash。主密钥仅保存在控制台,业务服务器不存储也不计算该签名。说明见 鉴权方式 A 与 配置 URL 鉴权。

Referer 防盗链使用白名单且不允许空 Referer。白名单匹配子域名且忽略端口(如 example.com 可匹配 foo.example.com 及任意端口)。空 Referer 或名单外请求返回 403,响应头为 X-Tengine-Error: denied by Referer ACL 且不返回跨域头。配置见 Referer 防盗链。

跨域响应头必须配置在加速域名的「缓存配置 → 节点 HTTP 响应头」并开启跨域验证(不能配在回源响应头)。Access-Control-Allow-Origin 精确到协议、主机与端口:

Text
http://localhost:5173,http://localhost:3000,https://example.com,https://www.example.com

转码模板组输出三档未加密 HLS:标清 848×478、高清 1280×720、超清 1920×1080。模板组 ID 写入环境变量;留空时请求不传 TemplateGroupId,回落至账号默认组。

RAM 子账号仅授予最小化权限:

JSON
{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "vod:CreateUploadVideo",
        "vod:RefreshUploadVideo",
        "vod:GetVideoInfo",
        "vod:GetPlayInfo",
        "vod:DeleteVideo"
      ],
      "Resource": "*"
    }
  ]
}

环境变量与依赖:

Bash
ALIYUN_VOD_ACCESS_KEY_ID=""
ALIYUN_VOD_ACCESS_KEY_SECRET=""
ALIYUN_VOD_REGION=cn-shanghai
ALIYUN_VOD_TEMPLATE_GROUP_ID=""
Bash
npm install aliyun-vod-upload-sdk@^2.0.0 video.js@^8.24.1

适配层与签名机制

适配层采用 RPC 风格调用 https://vod.{region}.aliyuncs.com。签名算法为 HMAC-SHA1。阿里云遵循 RFC 3986 规范,标准 encodeURIComponent 不转义的字符(!、'、(、)、*)需要二次编码,否则会触发 SignatureDoesNotMatch。

二次转义与 RPC 签名组装:

TypeScript
function percentEncode(str: string): string {
  return encodeURIComponent(str)
    .replace(/!/g, "%21").replace(/'/g, "%27")
    .replace(/\(/g, "%28").replace(/\)/g, "%29").replace(/\*/g, "%2A");
}

function buildSignedUrl(endpoint: string, accessKeyId: string, secret: string, action: string, params: Record<string, string>): string {
  const queryParams: Record<string, string> = {
    Format: "JSON", Version: "2017-03-21", AccessKeyId: accessKeyId,
    SignatureMethod: "HMAC-SHA1", SignatureVersion: "1.0",
    Timestamp: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
    SignatureNonce: crypto.randomUUID(), Action: action,
    ...params,
  };
  const canonicalQuery = Object.keys(queryParams).sort()
    .map((k) => `${percentEncode(k)}=${percentEncode(queryParams[k])}`).join("&");
  const stringToSign = `GET&${percentEncode("/")}&${percentEncode(canonicalQuery)}`;
  const signature = crypto.createHmac("sha1", `${secret}&`).update(stringToSign).digest("base64");
  return `${endpoint}/?${canonicalQuery}&Signature=${percentEncode(signature)}`;
}

适配层采用延迟加载,未配置密钥时不阻塞系统启动:

TypeScript
export function isAliyunVodConfigured(): boolean {
  return Boolean(process.env.ALIYUN_VOD_ACCESS_KEY_ID?.trim() && process.env.ALIYUN_VOD_ACCESS_KEY_SECRET?.trim());
}

export function getAliyunVodAdapter(): AliyunVodAdapter {
  if (!isAliyunVodConfigured()) throw new Error("未配置阿里云视频点播访问密钥");
  return new DefaultAliyunVodAdapter({
    accessKeyId: process.env.ALIYUN_VOD_ACCESS_KEY_ID!.trim(),
    accessKeySecret: process.env.ALIYUN_VOD_ACCESS_KEY_SECRET!.trim(),
    regionId: process.env.ALIYUN_VOD_REGION?.trim() || "cn-shanghai",
  });
}

获取播放信息时,PlayConfig 中固定传递加速域名,仅请求未加密的 m3u8 流,并按分辨率与码率从高到低排序,过滤加密流与非 video 类型流。

上传凭证与保存核验

签发上传凭证前执行资源鉴权与文件名清洗。文件名剔除路径前缀,扩展名仅允许 mp4、mov、m4v、avi、mkv、flv、webm、wmv,标题截断至 Unicode 128 字符。视频打上归属标签 res-${resourceId}。

TypeScript
export function checkOwnerTag(tags: string | undefined | null, resourceId: number): boolean {
  if (!tags) return false;
  return tags.split(",").map((t) => t.trim()).includes(`res-${resourceId}`);
}

转码状态归并规则:

  • Uploading、UploadSucc、Transcoding、Checking:归并为处理中(processing)。
  • Normal:归并为就绪(ready),并返回四舍五入后的时长秒数。
  • UploadFail、TranscodeFail、Blocked:归并为失败(failed)。

批量保存前执行差异比对与标签核验,避免冗余接口调用并防止越权引用:

TypeScript
export async function verifyVodRecords(resourceId: number, records: VodRecord[], existingRecords: VodRecord[]): Promise<VodRecord[]> {
  const existingMap = new Map(existingRecords.map((r) => [r.id, r.config]));
  const videoCache = new Map<string, { status: string; duration?: number; tags?: string }>();
  const adapter = getAliyunVodAdapter();

  const updated: VodRecord[] = [];
  for (const record of records) {
    const incomingId = record.config?.videoId?.trim();
    const existingId = existingMap.get(record.id)?.videoId;

    // 空记录保持空配置
    if (!incomingId) {
      updated.push({ ...record, config: {} });
      continue;
    }

    // 历史视频未变更:不调用外部接口,沿用已有时长与文件名
    if (existingId === incomingId) {
      updated.push(record);
      continue;
    }

    // 新增或变更视频:检查标签归属与云端状态(同批次缓存)
    let info = videoCache.get(incomingId);
    if (!info) {
      info = await adapter.getVideoInfo({ videoId: incomingId });
      videoCache.set(incomingId, info);
    }

    if (!checkOwnerTag(info.tags, resourceId)) {
      throw new Error("视频不属于此资源");
    }

    const durationSeconds = (info.status === "Normal" && info.duration) ? Math.round(info.duration) : undefined;
    updated.push({
      ...record,
      config: { ...record.config, videoId: incomingId, durationSeconds },
    });
  }
  return updated;
}

浏览器直传与状态轮询

上传 SDK 依赖浏览器环境,通过动态导入引入。直传逻辑包含取消上传时的延迟补删处理:

TypeScript
const { createUploader } = await import("aliyun-vod-upload-sdk");
let isCancelled = false;
let currentUploadingVideoId: string | null = null;

const uploader = createUploader({
  getAuth: async (ctx) => {
    if (ctx.kind === "create-video") {
      const res = await fetch("/api/aliyun-vod/upload-auth", {
        method: "POST", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ resourceId, fileName: (ctx.file as File).name }),
      });
      const data = await res.json();
      currentUploadingVideoId = data.videoId;

      // 凭证返回前若用户已触发取消,立即补发异步删除请求
      if (isCancelled) {
        fetch("/api/aliyun-vod/delete-video", {
          method: "POST", headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ resourceId, videoId: data.videoId }),
        });
        throw new Error("上传已取消");
      }
      return { UploadAuth: data.uploadAuth, UploadAddress: data.uploadAddress, VideoId: data.videoId };
    }
    // 刷新凭证逻辑
    const res = await fetch("/api/aliyun-vod/refresh-upload-auth", {
      method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ resourceId, videoId: ctx.videoId }),
    });
    const data = await res.json();
    return { UploadAuth: data.uploadAuth, UploadAddress: data.uploadAddress, VideoId: data.videoId };
  },
});

前端限制单个文件不超过 5 GB。取消上传时调用 uploadTask.abort() 并补调删除接口;离开页面通过 beforeunload 拦截。上传完成后开启 30 分钟轮询熔断机制:

TypeScript
function pollVideoStatus(resourceId: number, videoId: string, onReady: (duration?: number) => void) {
  const startTime = Date.now();
  const timer = setInterval(async () => {
    if (Date.now() - startTime > 30 * 60 * 1000) {
      clearInterval(timer); // 超时熔断,切换至手动重试
      return;
    }
    const res = await fetch("/api/aliyun-vod/video-status", {
      method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ resourceId, videoId }),
    });
    const data = await res.json();
    if (data.status === "ready") {
      clearInterval(timer);
      onReady(data.durationSeconds);
    } else if (data.status === "failed") {
      clearInterval(timer);
    }
  }, 5000);
}

播放地址门禁

播放地址接口(POST /api/aliyun-vod/play-url)执行严格分层校验,业务履约与准入鉴权前置于云厂商调用,避免非授权请求消耗配额:

  1. 参数校验:记录标识必须为正整数,否则返回 400「参数无效」。
  2. 登录认证:未登录返回 401「请先登录」。
  3. 记录存在性:记录不存在或类型不匹配返回 404「视频不存在」。
  4. 管理放行:资源所有者或管理员跳过后续业务状态与准入检查。
  5. 履约状态检查:异常履约状态(如退款处理中)返回 403「退款处理中,暂不可播放」。
  6. 准入权限检查:无授权权限返回 403「未获得观看权限」。
  7. 未配置检查:点播密钥缺失返回 200,reason 为 not_configured。
  8. 空视频检查:未绑定视频标识返回 200,reason 为 video_missing。
  9. OpenAPI 调用:调用 GetPlayInfo 获取流列表,若流未就绪再查 GetVideoInfo 区分处理中(processing)与转码失败(not_playable)。

多档 HLS 播放

各清晰度为独立 m3u8,播放端动态组装 Master Playlist 并生成 Blob URL 交由播放器加载:

TypeScript
function buildMasterPlaylist(renditions: PlayRendition[]): string {
  const lines = ["#EXTM3U", "#EXT-X-VERSION:3"];
  for (const r of renditions) {
    const bandwidth = r.bitrateKbps ? Math.round(r.bitrateKbps * 1000) : 1000000;
    const res = (r.width && r.height) ? `,RESOLUTION=${r.width}x${r.height}` : "";
    lines.push(`#EXT-X-STREAM-INF:BANDWIDTH=${bandwidth}${res}`);
    lines.push(r.playUrl);
  }
  return lines.join("\n");
}

Safari 默认将 HLS 移交系统播放器,无法识别 blob: 总列表。全平台统一采用 video.js 的 VHS 引擎接管,关闭原生音视频轨与文本轨(iPhone 开启 experimentalUseMMS):

TypeScript
const player = videojs(videoElement, {
  html5: {
    vhs: { overrideNative: true, ...(isIPhone ? { experimentalUseMMS: true } : {}) },
    nativeAudioTracks: false, nativeVideoTracks: false, nativeTextTracks: false,
  },
});

清晰度手选切换时,不更换播放地址或重建播放器,通过 VHS 双层 API 锁定目标档位:

TypeScript
function applyQualityLevel(player: any, renditions: PlayRendition[], targetQuality: string) {
  const isAuto = targetQuality === "auto";
  const target = renditions.find((r) => r.definition === targetQuality);

  // 1. 标准 qualityLevels API
  const qualityLevels = player.qualityLevels?.();
  if (qualityLevels) {
    for (let i = 0; i < qualityLevels.length; i++) {
      const ql = qualityLevels[i];
      ql.enabled = isAuto || (target && ql.height === target.height);
    }
  }

  // 2. 底层 VHS representations API 兼容
  try {
    const reps = player.tech({ IWillNotUseThisInPlugins: true })?.vhs?.representations?.();
    if (Array.isArray(reps)) {
      for (const rep of reps) {
        rep.enabled?.(isAuto || (target && rep.height === target.height));
      }
    }
  } catch {}
}

针对起播时播放进度被重置为 0 秒的问题,采用多事件核对状态机予以防护:

TypeScript
function setupPlaybackPositionGuard(player: any, startPosition: number) {
  let isStable = false;
  player.on(["ready", "loadedmetadata", "canplay", "timeupdate"], () => {
    if (startPosition <= 0 || isStable) return;
    const current = player.currentTime();
    if (current < 1 && Math.abs(current - startPosition) > 1) {
      player.currentTime(startPosition);
    } else if (current >= startPosition - 0.5) {
      isStable = true;
    }
  });
}

播放进度与完成判定

播放器与业务打点解耦,通过 10 秒心跳上报当前进度;当播放时长达到设定阈值(如 90%)时触发完成回调:

TypeScript
function setupVideoTracking(player: any, resourceId: number, duration: number, onComplete?: () => void) {
  let completed = false;
  let timer: NodeJS.Timeout | null = null;

  const report = (eventType: string) => {
    const position = Math.round(player.currentTime());
    fetch("/api/video-tracking", {
      method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ resourceId, eventType, positionSeconds: position }),
    }).catch(() => {});

    if (duration > 0 && !completed && position / duration >= 0.9) {
      completed = true;
      onComplete?.();
    }
  };

  player.on("play", () => { timer = setInterval(() => report("heartbeat"), 10000); });
  player.on(["pause", "ended"], (e: any) => {
    if (timer) clearInterval(timer);
    report(e.type);
  });
}

播放地址续签

CDN 改写后的 m3u8 中,每个分片携带独立的 auth_key,无法直接沿用主列表的查询参数。续签必须重新请求 GetPlayInfo 并通过文件名重新映射。

提前 1 分钟计算刷新时延:

TypeScript
function calculateRefreshDelay(urls: string[]): number {
  let earliestTs = Infinity;
  for (const url of urls) {
    const ts = Number(new URL(url).searchParams.get("auth_key")?.split("-")[0]);
    if (!Number.isNaN(ts)) earliestTs = Math.min(earliestTs, ts);
  }
  if (!Number.isFinite(earliestTs)) return 9 * 60 * 1000;
  return Math.max((earliestTs + 10 * 60 - 60) * 1000 - Date.now(), 1000);
}

获取新地址后,递归解析各档播放列表建立文件名映射,并通过 VHS xhr.onRequest 拦截改写后续请求:

TypeScript
export function rewritePlaybackRequest(input: { reqUri: string; renditions: { playUrl: string }[]; signedByFilename?: Map<string, string> }): string {
  try {
    const reqUrl = new URL(input.reqUri);
    const filename = reqUrl.pathname.split("/").pop() || "";
    for (const r of input.renditions) {
      if (filename === new URL(r.playUrl).pathname.split("/").pop()) return r.playUrl;
    }
    return input.signedByFilename?.get(filename) ?? input.reqUri;
  } catch {
    return input.reqUri;
  }
}

在 player.src() 调用前完成钩子挂载。若后续仍出现 403 鉴权失败,清空旧映射后重新执行换源。

内容加密选型与 HLS 标准加密演进

当前落地方案采用未加密 HLS 搭配 URL 鉴权与 Referer 防盗链。需要明确的是,未加密方案并不能满足对媒体分片防下载、防离线传播的安全要求:URL 鉴权和 Referer 白名单仅限制了播放地址的请求时效与合法来源,一旦授权用户在有效期内提取到 TS 分片,由于分片本身为明文,仍可被离线播放或抓包保存。当前采用未加密方案,是基于系统初期快速跑通直传、转码与播放核心链路的工程取舍。

点播支持的三类内容安全方案对比:

方案 加密机制 播放端要求 适用性与成本
阿里云私有加密 私有算法,一文一密 仅支持阿里云播放器 Web 端无法直接播 URL,费用同通用加密
HLS 标准加密 AES-128 信封加密 支持标准 EXT-X-KEY(含 video.js) 需自建 KMS 解密服务与令牌服务
商业 DRM FairPlay / Widevine 平台级原生解密环境 License 按次计费,接入链条长

阿里云私有加密采用私有算法,文档明确要求 Web 端播放必须依赖播放凭证与阿里云专有播放器 SDK;商业 DRM(如 FairPlay、Widevine)依赖平台底层硬件解密和 License 授权服务,开发维护成本高昂。而 HLS 标准加密,是后续强防下载诉求时的平滑演进项,也是阿里云 VOD 官网推荐的最佳实践方式。

HLS 标准加密遵循 RFC 8216 规范,使用 AES-128 对各 TS 分片逐个加密。核心流转包含三个环节:

  1. 转码与信封加密:视频上传后,点播调用 KMS 的 GenerateDataKey 接口生成数据密钥。密文密钥保存在媒资元数据中,转码作业使用明文密钥对视频分片执行 AES-128 加密。
  2. 播放列表重写:转码输出的 m3u8 中插入 EXT-X-KEY:METHOD=AES-128,URI="https://decrypt.example.com/...",指向业务自建的解密密钥分发服务。
  3. 令牌校验与解密播放:加速域名开启参数透传后,会将播放地址中携带的业务令牌(MtsHlsUriToken)透传至解密 URI。解密服务验证令牌合法性后,调用 KMS 解密接口恢复出 16 字节明文密钥并返回给客户端。video.js 原生支持标准的 AES-128 解密,无需更换播放内核即可完成播放。流程参考 HLS 标准加密。

后续引入 HLS 标准加密时,现有架构的改造范围集中在以下五个节点:

  • 转码触发路径:上传后的自动转码模板不支持动态标准加密。需调整为上传时绑定不转码模板,上传成功后通过 OpenAPI 手动提交转码作业并传入加密配置。存量未加密视频需重新提交转码作业。
  • 密钥与解密服务:在与点播存储桶相同地域创建 KMS Service Key。自建解密服务与令牌校验接口,并在接口上开启对站点来源的 CORS 跨域响应头。
  • CDN 域名配置:加速域名开启 HLS 标准加密参数透传,使播放地址上的令牌能被改写并透传到 EXT-X-KEY 声明的解密 URI。
  • 适配层流过滤:修改适配层中的流校验逻辑,不仅放行未加密流,同时放行 Encrypt === 1 且 EncryptType === "HLSEncryption" 的流,继续过滤私有加密与非 m3u8 流:
TypeScript
function isPlayableStream(item: {
  PlayURL?: string;
  Format?: string;
  StreamType?: string;
  Encrypt?: number | string;
  EncryptType?: string;
}): boolean {
  if (!item.PlayURL) return false;
  if (item.Format?.toLowerCase() !== "m3u8") return false;
  if (item.StreamType?.toLowerCase() !== "video") return false;

  const isEncrypted = item.Encrypt !== undefined && item.Encrypt !== null && String(item.Encrypt) !== "0";
  if (!isEncrypted) return true;

  return item.EncryptType === "HLSEncryption";
}
  • 播放端兼容:VHS 引擎在解析到 EXT-X-KEY 时会自动发起密钥请求。现有的 Master Playlist 拼装、清晰度锁定和续签改写钩子均可沿用,需注意解密 URI 同样具有独立路径,不能套用 m3u8 的 auth_key 查询串。

废弃视频清理与生产注意事项

视频删除调用 DeleteVideo,分批处理(单批上限 20 个),删除失败不阻断业务数据提交流程:

TypeScript
export async function cleanupOrphanVideos(resourceId: number, candidateVideoIds: string[]): Promise<string | undefined> {
  const uniqueIds = Array.from(new Set(candidateVideoIds.filter(Boolean)));
  if (uniqueIds.length === 0 || !isAliyunVodConfigured()) return;

  const adapter = getAliyunVodAdapter();
  const toDelete: string[] = [];
  let hasWarning = false;

  for (const vid of uniqueIds) {
    // 1. 全站引用检查:全站已有记录引用的视频保留不删
    if (await checkIsVideoReferencedInAnySavedRecord(vid)) continue;

    try {
      const info = await adapter.getVideoInfo({ videoId: vid });
      // 2. 标签校验:非本资源视频不删
      if (checkOwnerTag(info?.tags, resourceId)) toDelete.push(vid);
    } catch (err: any) {
      if (err.code === "NoSuchVideo") continue;
      hasWarning = true;
    }
  }

  // 3. 分批调用删除
  for (let i = 0; i < toDelete.length; i += 20) {
    try {
      await adapter.deleteVideos({ videoIds: toDelete.slice(i, i + 20) });
    } catch {
      hasWarning = true;
    }
  }

  return hasWarning ? "内容已保存,旧视频未能从阿里云删除,将继续占用存储。" : undefined;
}

生产注意事项清单:

  • 密钥保护:访问密钥仅存放在运行环境,禁止打包至前端代码中。
  • 播放域名一致性:适配层 PlayConfig.PlayDomain 必须与控制台加速域名完全一致。
  • 模板组绑定:生产环境必须配置多档 HLS 模板组 ID,避免落入单档默认模板。
  • Safari 与移动端限制:全平台开启 VHS 接管后不提供 AirPlay;微信与旧版 iOS 主动降级为单路播放。
  • 删除失败容错:清理逻辑失败仅返回非阻塞警告,不得回滚已成功的数据库事务。
目录
方案架构与核心组件控制台与运行环境适配层与签名机制上传凭证与保存核验浏览器直传与状态轮询播放地址门禁多档 HLS 播放播放进度与完成判定播放地址续签内容加密选型与 HLS 标准加密演进废弃视频清理与生产注意事项
上一篇Creem 支付结算全流程实战笔记

©2015-2026 黑白梦 粤ICP备15018165号

联系: heibaimeng@foxmail.com