先把demo代码占出来,改天有空写逻辑
package com.cjbdi.test;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.kms.model.v20160120.GenerateDataKeyRequest;
import com.aliyuncs.kms.model.v20160120.GenerateDataKeyResponse;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.vod.model.v20170321.GenerateKMSDataKeyRequest;
import com.aliyuncs.vod.model.v20170321.GenerateKMSDataKeyResponse;
import com.aliyuncs.vod.model.v20170321.SubmitTranscodeJobsRequest;
import com.aliyuncs.vod.model.v20170321.SubmitTranscodeJobsResponse;
import com.alibaba.fastjson.JSONObject;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
public class SubmitEncryptTranscodeJob {
private static DefaultAcsClient kmsClient;
private static DefaultAcsClient vodClient;
static {
String REGION_ID = "cn-beijing"; // 阿里云区域ID
String accessKeyId = "";
String accessKeySecret ="";
// 初始化KMS客户端
DefaultProfile kmsProfile = DefaultProfile.getProfile(REGION_ID, accessKeyId, accessKeySecret);
kmsClient = new DefaultAcsClient(kmsProfile);
// 初始化VOD客户端
DefaultProfile vodProfile = DefaultProfile.getProfile(REGION_ID, accessKeyId, accessKeySecret);
vodClient = new DefaultAcsClient(vodProfile);
}
/**
* 提交HLS加密转码任务
*
* @param videoId 视频ID(从上传回调获取)
* @param templateGroupId HLS加密模板组ID
* @param decryptServiceUrl 解密服务地址 (e.g., "http://your-server:8099/decrypt")
* @return 转码任务响应
*/
public static SubmitTranscodeJobsResponse submitHlsEncryptJob(
String videoId,
String templateGroupId,
String decryptServiceUrl) throws ClientException {
// // 1. 生成KMS数据密钥
// GenerateDataKeyRequest keyRequest = new GenerateDataKeyRequest();
// // 设置密钥规格为AES_256(阿里云VOD要求)
//// keyRequest.setKeySpec("AES_256");
// GenerateDataKeyResponse keyResponse = kmsClient.getAcsResponse(keyRequest);
GenerateKMSDataKeyRequest request = new GenerateKMSDataKeyRequest();
GenerateKMSDataKeyResponse keyResponse = kmsClient.getAcsResponse(request);
System.out.println("获取ciphertextBlob");
String ciphertextBlob = keyResponse.getCiphertextBlob();
System.out.println("CiphertextBlob Value: " + ciphertextBlob); // <-- 添加这行日志
// 2. 构建加密配置
JSONObject encryptConfig = new JSONObject();
encryptConfig.put("CipherText", keyResponse.getCiphertextBlob()); // 信封加密密钥(EDK)
System.out.println("EncryptConfig JSONObject: " + encryptConfig.toJSONString()); // <-- 添加这行日志
// 使用 UTF-8 编码进行 URL 编码
String encodedCipherText;
try {
// 使用 UTF-8 编码进行 URL 编码
encodedCipherText = URLEncoder.encode(ciphertextBlob, StandardCharsets.UTF_8.toString());
} catch (Exception e) {
throw new RuntimeException("Failed to encode CiphertextBlob for URL", e);
}
String fullDecryptKeyUri = decryptServiceUrl + "?CipherText=" + encodedCipherText;
System.out.println("Constructed Full DecryptKeyUri: " + fullDecryptKeyUri);
// 3. 构建加密配置 (注意 CipherText 字段现在是可选的,因为 URI 已经包含了它)
// encryptConfig.put("CipherText", ciphertextBlob); // 可以保留,也可以不保留,取决于 VOD 要求
encryptConfig.put("DecryptKeyUri", fullDecryptKeyUri); // 使用拼接好的完整 URI
encryptConfig.put("KeyServiceType", "KMS"); // 加密类型
// 3. 提交转码任务
SubmitTranscodeJobsRequest transcodeRequest = new SubmitTranscodeJobsRequest();
transcodeRequest.setVideoId(videoId);
transcodeRequest.setTemplateGroupId(templateGroupId);
transcodeRequest.setEncryptConfig(encryptConfig.toJSONString());
System.out.println("Final EncryptConfig JSON String: " + encryptConfig.toJSONString()); // <-- 添加这行日志
return vodClient.getAcsResponse(transcodeRequest);
}
public static void main(String[] args) {
try {
// 示例参数 - 实际使用中应从配置或参数传入
String videoId = "40bafb848b1171f080a77fb2780c0102";
String templateGroupId = "610e0d1c7b9ed52d6974d72dd2075743";
//http://g5w6p0oa.dongtaiyuming.net/decrypt?CipherText=123.&Token=123
String decryptServiceUrl = "https://show.syntheticreality.cn/hd-api/decrypt";
SubmitTranscodeJobsResponse response = submitHlsEncryptJob(
videoId, templateGroupId, decryptServiceUrl);
System.out.println("转码任务提交成功!");
System.out.println("请求ID: " + response.getRequestId());
System.out.println("转码任务ID: " + response.getTranscodeTaskId());
} catch (ClientException e) {
System.err.println("转码任务提交失败:");
System.err.println("错误代码: " + e.getErrCode());
System.err.println("错误消息: " + e.getErrMsg());
System.err.println("请求ID: " + e.getRequestId());
}
}
}
package com.cjbdi.test;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.ProtocolType;
import com.aliyuncs.profile.DefaultProfile;
// 注意:导入的是 VOD 的 DecryptKMSDataKeyRequest/Response
import com.aliyuncs.vod.model.v20170321.DecryptKMSDataKeyRequest;
import com.aliyuncs.vod.model.v20170321.DecryptKMSDataKeyResponse;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import org.apache.commons.codec.binary.Base64; // 确保引入了 commons-codec
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLDecoder; // 导入 URLDecoder
import java.nio.charset.StandardCharsets; // 导入 StandardCharsets
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class HlsDecryptServer {
private static final int PORT = 8099;
private static final String CONTEXT_PATH = "/decrypt"; // 定义一个明确的上下文路径
private static DefaultAcsClient client;
static {
// --- 配置 ---
// KMS的区域,必须与视频对应区域
// 访问KMS的授权AccessKey信息
// 强烈建议不要把AccessKey ID和AccessKey Secret保存到工程代码里。
// 本示例为了演示方便,直接写在代码里。生产环境请从环境变量或配置文件读取。
String region = "cn-beijing"; // 阿里云区域ID
String accessKeyId = "LTAI5tFzxxuoLfUEF2UzrCsB";
String accessKeySecret ="zTZkmMSc0WoGR6nhIh7cLbfYWsIQ8S";
// 检查凭据是否设置
if (accessKeyId == null || accessKeySecret == null || accessKeyId.isEmpty() || accessKeySecret.isEmpty()) {
System.err.println("错误: 请设置环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID 和 ALIBABA_CLOUD_ACCESS_KEY_SECRET。");
// System.exit(1); // 或者抛出异常阻止服务器启动
client = null; // 标记客户端初始化失败
} else {
try {
DefaultProfile profile = DefaultProfile.getProfile(region, accessKeyId, accessKeySecret);
client = new DefaultAcsClient(profile);
System.out.println("HlsDecryptServerNoToken: VOD KMS Client initialized for region: " + region);
} catch (Exception e) {
System.err.println("HlsDecryptServerNoToken: Failed to initialize VOD KMS Client: " + e.getMessage());
e.printStackTrace();
client = null;
}
}
}
/**
* 说明:
* 1、接收解密请求,获取密文密钥和令牌Token
* 2、调用VOD KMS decrypt接口获取明文密钥
* 3、将明文密钥base64decode返回
*/
public class HlsDecryptHandler implements HttpHandler {
/**
* 处理解密请求
* @param httpExchange
* @throws IOException
*/
@Override // 添加 @Override 注解
public void handle(HttpExchange httpExchange) throws IOException {
System.out.println("\n--- Received new request ---");
System.out.println("Request URI: " + httpExchange.getRequestURI());
System.out.println("Request Method: " + httpExchange.getRequestMethod());
Headers responseHeaders = httpExchange.getResponseHeaders();
// 设置 CORS 头
responseHeaders.add("Access-Control-Allow-Origin", "*");
responseHeaders.add("Access-Control-Allow-Methods", "GET, OPTIONS");
responseHeaders.add("Access-Control-Allow-Headers", "*");
String requestMethod = httpExchange.getRequestMethod();
// 处理 OPTIONS 预检请求
if ("OPTIONS".equalsIgnoreCase(requestMethod)) {
httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, -1); // 204 No Content
httpExchange.close(); // 关闭交换
System.out.println("Handled OPTIONS preflight request.");
return;
}
if ("GET".equalsIgnoreCase(requestMethod)) {
try {
// 从URL中取得密文密钥
String ciphertext = getCiphertext(httpExchange);
System.out.println("Extracted CipherText: " + ciphertext);
if (ciphertext == null || ciphertext.isEmpty()) {
sendErrorResponse(httpExchange, HttpURLConnection.HTTP_BAD_REQUEST, "Missing or empty CipherText parameter.");
return;
}
if (client == null) {
sendErrorResponse(httpExchange, HttpURLConnection.HTTP_INTERNAL_ERROR, "Server not properly configured (KMS client is null).");
return;
}
// 从KMS中解密出来,并Base64 decode
byte[] key = decrypt(ciphertext);
if (key == null) {
// decrypt 方法内部已打印错误,这里只需发送错误响应
sendErrorResponse(httpExchange, HttpURLConnection.HTTP_INTERNAL_ERROR, "Failed to decrypt key using KMS.");
return;
}
// 设置header
setHeader(httpExchange, key);
// 返回base64decode之后的密钥
try (OutputStream responseBody = httpExchange.getResponseBody()) { // 使用 try-with-resources 确保 OutputStream 关闭
responseBody.write(key);
}
System.out.println("Decrypted key sent successfully.");
} catch (Exception e) {
System.err.println("Internal server error during request handling: " + e.getMessage());
e.printStackTrace();
// 即使发生异常,也要确保 HttpExchange 被关闭
if (!httpExchange.getResponseHeaders().containsKey("Content-Type")) { // 检查是否已发送响应头
sendErrorResponse(httpExchange, HttpURLConnection.HTTP_INTERNAL_ERROR, "Internal Server Error: " + e.getMessage());
}
} finally {
// 最终确保 HttpExchange 被关闭
httpExchange.close();
System.out.println("--- Request processing finished ---\n");
}
} else {
// 不支持的方法
sendErrorResponse(httpExchange, HttpURLConnection.HTTP_BAD_METHOD, "Method Not Allowed. Only GET is supported.");
// sendErrorResponse 内部会关闭 httpExchange
}
}
/**
* 发送错误响应
* @param exchange HttpExchange 对象
* @param statusCode HTTP 状态码
* @param message 错误信息
*/
private void sendErrorResponse(HttpExchange exchange, int statusCode, String message) {
try {
System.err.println("Sending error response (" + statusCode + "): " + message);
byte[] response = message.getBytes(StandardCharsets.UTF_8);
Headers headers = exchange.getResponseHeaders();
headers.set("Content-Type", "text/plain; charset=utf-8");
headers.set("Access-Control-Allow-Origin", "*"); // 确保错误响应也有 CORS 头
exchange.sendResponseHeaders(statusCode, response.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(response);
}
} catch (IOException e) {
System.err.println("Failed to send error response: " + e.getMessage());
e.printStackTrace();
} finally {
// 确保交换被关闭
exchange.close();
}
}
private void setHeader(HttpExchange httpExchange, byte[] key) throws IOException {
Headers responseHeaders = httpExchange.getResponseHeaders();
responseHeaders.set("Content-Type", "application/octet-stream");
responseHeaders.set("Access-Control-Allow-Origin", "*");
// 可选:添加缓存控制头
// responseHeaders.set("Cache-Control", "no-store");
httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, key.length);
}
/**
* 调用VOD KMS decrypt接口解密,并将明文base64decode
* @param ciphertext Base64 encoded ciphertext
* @return Decrypted key bytes, or null if failed
*/
private byte[] decrypt(String ciphertext) {
DecryptKMSDataKeyRequest request = new DecryptKMSDataKeyRequest();
request.setCipherText(ciphertext); // VOD SDK 期望接收 Base64 字符串
request.setProtocol(ProtocolType.HTTPS);
try {
System.out.println("Calling VOD KMS Decrypt API...");
DecryptKMSDataKeyResponse response = client.getAcsResponse(request);
String plaintextBase64 = response.getPlaintext(); // VOD SDK 返回 Base64 String
System.out.println("KMS Decrypted Plaintext (Base64): " + plaintextBase64);
if (plaintextBase64 == null || plaintextBase64.isEmpty()) {
System.err.println("Warning: KMS returned null or empty plaintext.");
return null;
}
// 注意:需要base64 decode
byte[] keyBytes = Base64.decodeBase64(plaintextBase64); // 使用 commons-codec
System.out.println("Plaintext decoded to " + keyBytes.length + " bytes.");
return keyBytes;
} catch (ClientException e) {
System.err.println("KMS Decrypt API call failed:");
System.err.println("Error Code: " + e.getErrCode());
System.err.println("Error Message: " + e.getMessage());
System.err.println("Request ID: " + e.getRequestId()); // 打印 Request ID 有助于排查
// e.printStackTrace(); // 可以选择打印完整堆栈
return null;
}
}
/**
* 从URL中获取密文密钥参数 (修正版)
* @param httpExchange
* @return Base64 encoded ciphertext, or null if not found
*/
private String getCiphertext(HttpExchange httpExchange) {
URI uri = httpExchange.getRequestURI();
String queryString = uri.getQuery();
System.out.println("Raw Query String: " + queryString);
if (queryString == null || queryString.isEmpty()) {
System.out.println("Query string is null or empty.");
return null;
}
// --- 修正 1: URL 解码 ---
try {
queryString = URLDecoder.decode(queryString, StandardCharsets.UTF_8.name());
System.out.println("URL Decoded Query String: " + queryString);
} catch (Exception e) { // 包括 UnsupportedEncodingException
System.err.println("Failed to URL decode query string: " + e.getMessage());
// 即使解码失败,也尝试用原始字符串匹配
}
// --- 修正 2: 更宽松的正则表达式 ---
// 匹配 CipherText= 后面的任何非 & 字符 (包括 Base64 字符)
String pattern = "CipherText=([^&]*)";
Pattern r = Pattern.compile(pattern);
Matcher m = r.matcher(queryString);
if (m.find()) {
String cipherTextValue = m.group(1);
System.out.println("Matched CipherText value: " + cipherTextValue);
return cipherTextValue; // 返回匹配到的值
} else {
System.out.println("CipherText parameter not found in query string using regex: " + pattern);
return null;
}
}
}
/**
* 服务启动
*
* @throws IOException
*/
public void serviceBootStrap() throws IOException {
if (client == null) {
throw new IOException("VOD KMS Client failed to initialize, cannot start server.");
}
// 监听端口可以自定义,能同时接受最多30个请求
HttpServer httpserver = HttpServer.create(new InetSocketAddress(PORT), 30);
httpserver.createContext(CONTEXT_PATH, new HlsDecryptHandler()); // 使用定义的路径
// 可以设置线程池
// httpserver.setExecutor(Executors.newFixedThreadPool(10));
httpserver.start();
System.out.println("===================================================");
System.out.println("HLS VOD KMS 解密服务已启动");
System.out.println("监听端口: " + PORT);
System.out.println("解密端点: http://<your-server-ip>:" + PORT + CONTEXT_PATH + "?CipherText=BASE64_ENCODED_CIPHERTEXT");
System.out.println("===================================================");
}
// public static void main(String[] args) throws IOException {
// HlsDecryptServerNoToken server = new HlsDecryptServerNoToken();
// server.serviceBootStrap();
// System.out.println("服务器启动成功,按 Ctrl+C 停止服务...");
// // 可以添加一个关闭钩子来优雅地停止服务器
// Runtime.getRuntime().addShutdownHook(new Thread(() -> {
// System.out.println("\n正在关闭服务器...");
// // 如果需要停止 HttpServer, 可以保存 httpserver 实例并调用 httpserver.stop(0);
// System.out.println("服务器已关闭。");
// }));
// }
}
前端
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>艾达纪元</title>
<link rel="stylesheet" href="https://g.alicdn.com/apsara-media-box/imp-web-player/2.16.3/skins/default/aliplayer-min.css">
<script charset="utf-8" src="https://g.alicdn.com/apsara-media-box/imp-web-player/2.16.3/aliplayer-min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif;
padding: 20px;
background: linear-gradient(135deg, #0c0e1a 0%, #1a1c2c 100%);
color: #e0e0ff;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
margin-bottom: 30px;
text-align: center;
padding: 25px 0;
background: rgba(30, 32, 48, 0.6);
border-radius: 16px;
backdrop-filter: blur(10px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.25);
border: 1px solid rgba(142, 231, 255, 0.15);
}
.header h1 {
font-size: 32px;
margin-bottom: 10px;
font-weight: 700;
letter-spacing: 1px;
background: linear-gradient(90deg, #8ee7ff, #6cb2ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 15px rgba(142, 231, 255, 0.6);
}
.subtitle {
font-size: 18px;
color: #a0a0d0;
max-width: 800px;
margin: 0 auto;
line-height: 1.6;
font-weight: 300;
}
/* 播放器网格容器 */
.players-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 25px;
margin-bottom: 30px;
}
/* 单个播放器容器 */
.player-container {
position: relative;
background: rgba(20, 22, 36, 0.8);
border-radius: 16px;
overflow: hidden;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.4);
transition: all 0.3s ease;
border: 1px solid rgba(142, 231, 255, 0.1);
}
.player-container:hover {
transform: translateY(-5px);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.5), 0 0 20px rgba(142, 231, 255, 0.3);
border-color: rgba(142, 231, 255, 0.3);
}
/* 播放器标题 */
.player-title {
padding: 15px 20px;
background: rgba(42, 44, 66, 0.7);
font-size: 18px;
font-weight: 600;
color: #e0e0ff;
display: flex;
align-items: center;
min-height: 60px;
}
.player-title i {
margin-right: 10px;
color: #8ee7ff;
font-size: 20px;
}
/* 播放器包装 */
.player-wrapper {
position: relative;
width: 100%;
height: 0;
padding-bottom: 56.25%; /* 16:9 宽高比 */
}
.player-wrapper-inner {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #000;
}
/* 加载状态 */
.loading-state {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(10, 12, 20, 0.9);
z-index: 10;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(142, 231, 255, 0.2);
border-radius: 50%;
border-top-color: #8ee7ff;
animation: spin 1s ease-in-out infinite;
margin-bottom: 15px;
}
.status-message {
font-size: 16px;
text-align: center;
color: #e0e0ff;
padding: 0 20px;
}
/* 控制区域 */
.controls {
display: flex;
justify-content: center;
gap: 20px;
padding: 15px 20px;
background: rgba(30, 32, 48, 0.6);
border-radius: 12px;
margin-bottom: 20px;
border: 1px solid rgba(142, 231, 255, 0.1);
}
.control-btn {
background: linear-gradient(135deg, #3c5aa0, #2a3d80);
color: white;
border: none;
padding: 10px 22px;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
display: flex;
align-items: center;
font-size: 16px;
min-width: 160px;
justify-content: center;
}
.control-btn i {
margin-right: 8px;
}
.control-btn:hover {
background: linear-gradient(135deg, #4a6cc0, #364b8a);
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}
/* 状态信息 */
.status-info {
text-align: center;
padding: 15px;
color: #8a8ac4;
font-size: 16px;
background: rgba(30, 32, 48, 0.5);
border-radius: 10px;
margin-top: 20px;
border: 1px solid rgba(142, 231, 255, 0.1);
}
/* 响应式设计 */
@media (max-width: 900px) {
.players-grid {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 600px) {
.players-grid {
grid-template-columns: 1fr;
}
.header h1 {
font-size: 26px;
}
.subtitle {
font-size: 16px;
}
.control-btn {
min-width: 140px;
padding: 8px 16px;
font-size: 14px;
}
}
/* 动画 */
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.player-container {
animation: fadeIn 0.5s ease-out forwards;
}
/* 页脚 */
.footer {
text-align: center;
margin-top: 40px;
padding: 20px;
color: #8a8ac4;
font-size: 14px;
border-top: 1px solid rgba(142, 231, 255, 0.1);
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>全双工数字人案例</h1>
<!-- <div class="subtitle">探索人工智能与人类交互的未来,体验前沿数字人技术</div> -->
</div>
<!-- 控制区域 -->
<div class="controls">
<button class="control-btn" id="pauseAllBtn">
<i class="fas fa-pause"></i> 暂停所有
</button>
<!-- <button class="control-btn" id="muteAllBtn">
<i class="fas fa-volume-mute"></i> 全部静音
</button> -->
<button class="control-btn" id="refreshBtn">
<i class="fas fa-sync-alt"></i> 刷新数据
</button>
</div>
<!-- 播放器网格 -->
<div id="playersGrid" class="players-grid">
<!-- 播放器将动态生成 -->
</div>
<div id="statusInfo" class="status-info">
正在加载视频数据...
</div>
<div class="footer">
© 2025 艾达纪元
</div>
</div>
<script>
// 全局变量
let players = [];
let videoData = [];
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
// 加载播放器资源
loadPlayerResources();
// 添加按钮事件监听
document.getElementById('pauseAllBtn').addEventListener('click', pauseAllPlayers);
document.getElementById('muteAllBtn').addEventListener('click', toggleMuteAll);
document.getElementById('refreshBtn').addEventListener('click', refreshData);
});
// 检测苹果设备
function isAppleDevice() {
const userAgent = navigator.userAgent;
return /iPhone|iPad|iPod|Macintosh|MacIntel|MacPPC|Mac68K/i.test(userAgent);
}
// 1. 加载播放器资源
function loadPlayerResources() {
updateStatus('正在初始化播放环境...');
// 检查是否已经加载了播放器脚本
if (typeof Aliplayer !== 'undefined') {
initPlayerData();
return;
}
// 创建脚本加载监听
const script = document.createElement('script');
script.src = 'https://g.alicdn.com/apsara-media-box/imp-web-player/2.16.3/aliplayer-min.js';
script.charset = 'utf-8';
script.onload = function() {
console.log('播放器脚本加载完成');
initPlayerData();
};
script.onerror = function() {
showError('播放器脚本加载失败,请检查网络连接');
};
document.head.appendChild(script);
}
// 2. 初始化视频数据
function initPlayerData() {
updateStatus('正在获取视频数据...');
// 获取视频数据
fetch('https://ip/api/open/tls/db')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP 错误! 状态码: ${response.status}`);
}
return response.json();
})
.then(result => {
if (result.code === 200 && result.data && result.data.length > 0) {
videoData = result.data;
createPlayerContainers();
initPlayers();
} else {
throw new Error(result.msg || '接口返回数据格式异常');
}
})
.catch(error => {
console.error('获取视频数据失败:', error);
showError(`获取视频数据失败: ${error.message}`);
});
}
// 3. 创建播放器容器
function createPlayerContainers() {
const playersGrid = document.getElementById('playersGrid');
playersGrid.innerHTML = '';
// 更新状态信息
updateStatus(`已加载 ${videoData.length} 个视频`);
// 为每个视频创建容器
videoData.forEach((video, index) => {
const playerContainer = document.createElement('div');
playerContainer.className = 'player-container';
playerContainer.innerHTML = `
<div class="player-title">
<i class="fas fa-video"></i> ${video.tittle || `数字人视频 ${index + 1}`}
</div>
<div class="player-wrapper">
<div class="player-wrapper-inner">
<div id="player-${index}"></div>
<div class="loading-state">
<div class="loading-spinner"></div>
<div class="status-message">加载视频中...</div>
</div>
</div>
</div>
`;
playersGrid.appendChild(playerContainer);
});
}
// 4. 初始化播放器
function initPlayers() {
players = [];
videoData.forEach((video, index) => {
try {
// 创建新播放器
const player = new Aliplayer({
id: `player-${index}`,
width: '100%',
height: '100%',
vid: video.vid,
playauth: video.playauth,
encryptType: 1, // 私有加密流
qualitySort: 'asc',
format: 'm3u8',
mediaType: 'video',
encryptType: 1,
width: '100%',
autoplay: true,
isLive: false,
rePlay: false,
videoHeight: undefined,
isVBR: undefined,
preload: true,
controlBarVisibility: 'hover',
useH5Prism: true
}, function(playerInstance) {
console.log(`播放器 ${index} 创建成功`);
// 隐藏加载状态
const container = document.querySelector(`#player-${index}`).parentNode;
const loadingState = container.querySelector('.loading-state');
if (loadingState) {
loadingState.style.display = 'none';
}
// 监听播放器事件
player.on('play', function() {
console.log(`播放器 ${index} 开始播放`);
});
player.on('pause', function() {
console.log(`播放器 ${index} 暂停播放`);
});
player.on('error', function(e) {
console.error(`播放器 ${index} 错误:`, e);
showError(`播放器 ${index+1} 错误: ${e.paramData.errorMsg || '未知错误'}`);
});
});
players.push(player);
} catch (e) {
console.error(`播放器 ${index} 创建失败`, e);
showError(`播放器 ${index+1} 初始化失败: ${e.message}`);
}
});
}
// 5. 控制功能
function pauseAllPlayers() {
players.forEach(player => {
if (player && player.pause) {
player.pause();
}
});
}
function toggleMuteAll() {
const btn = document.getElementById('muteAllBtn');
const isMuted = players.length > 0 ? players[0].isMuted() : false;
players.forEach(player => {
if (player && player.mute) {
player.mute(!isMuted);
}
});
// 更新按钮文本
btn.innerHTML = isMuted ?
'<i class="fas fa-volume-mute"></i> 全部静音' :
'<i class="fas fa-volume-up"></i> 取消静音';
}
function refreshData() {
players.forEach(player => {
if (player && player.dispose) {
player.dispose();
}
});
players = [];
updateStatus('正在刷新数据...');
initPlayerData();
}
// 6. 状态管理
function updateStatus(message) {
const statusElement = document.getElementById('statusInfo');
if (statusElement) {
statusElement.textContent = message;
}
}
function showError(message) {
const statusElement = document.getElementById('statusInfo');
if (statusElement) {
statusElement.textContent = message;
statusElement.style.color = '#ff7d9c';
}
}
</script>
</body>
</html>