标签搜索

目 录CONTENT

文章目录

阿里云标准加密hls【支持苹果手机和安卓手机以及web】

高大北
2025-09-07 / 0 评论 / 0 点赞 / 13 阅读 / 4,400 字 / 正在检测是否收录...

先把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>
0
博主关闭了所有页面的评论