<?php /** * Plugin Name: 客信云点播 * Plugin URI: https://www.psste.com * Description: 腾讯云点播 — 视频上传·自动转码·TCPlayer播放·防盗链·短代码 * Version: 2.0 * Author: 客信新材料 * License: GPLv2 * Text Domain: kexin-vod */ if (!defined('ABSPATH')) exit; define('KEXIN_VOD_URL', plugin_dir_url(__FILE__)); define('KEXIN_VOD_VER', '2.0'); // ====== 管理菜单 ====== add_action('admin_menu', function() { add_menu_page( '客信云点播', '云点播', 'manage_options', 'kexin-vod', 'kexin_vod_page_upload', 'dashicons-video-alt3', 30 ); add_submenu_page('kexin-vod', '上传视频', '上传视频', 'manage_options', 'kexin-vod', 'kexin_vod_page_upload'); add_submenu_page('kexin-vod', '视频库', '视频库', 'manage_options', 'kexin-vod-library', 'kexin_vod_page_library'); add_submenu_page('kexin-vod', '系统设置', '系统设置', 'manage_options', 'kexin-vod-settings', 'kexin_vod_page_settings'); }); // ====== 上传页面 ====== function kexin_vod_page_upload() { ?> <div class="wrap"> <h1>上传视频</h1> <p class="description">上传视频到腾讯云点播,自动触发转码和封面截图。支持 MP4/MOV/AVI/FLV,最大 50GB。</p> <script src="https://cdn-go.cn/cdn/vod-js-sdk-v6/latest/vod-js-sdk-v6.js"></script> <div id="kexin-upload-wrap" style="max-width:700px;margin-top:20px"> <div id="kexin-dropzone" style="border:2px dashed #c0c0c0;border-radius:8px;padding:48px 24px;text-align:center;cursor:pointer;background:#fafafa;transition:all 0.2s"> <div style="font-size:3rem;margin-bottom:8px">&#9654;</div> <p style="font-size:1.2rem;font-weight:600;margin:0">点击或拖拽视频文件到此处</p> <p style="color:#888;margin:4px 0 0">支持格式:MP4 / MOV / AVI / FLV · 单文件最大 50GB</p> <input type="file" id="kexin-file-input" accept="video/*" style="display:none"> </div> <div id="kexin-name-wrap" style="display:none;margin-top:14px"> <label style="font-weight:600;display:block;margin-bottom:4px">视频名称</label> <input id="kexin-vname" class="regular-text" style="width:100%" placeholder="输入视频名称(可选,默认使用文件名)"> </div> <div id="kexin-progress" style="display:none;margin-top:14px"> <div style="background:#e0e0e0;border-radius:4px;height:10px;overflow:hidden"> <div id="kexin-bar" style="background:#F97316;height:100%;width:0%;transition:width 0.3s"></div> </div> <p style="margin-top:8px;font-size:13px"> <strong id="kexin-status">准备中...</strong> &nbsp; <span id="kexin-pct">0%</span> &nbsp; <span id="kexin-speed" style="color:#888"></span> </p> </div> <div id="kexin-result" style="display:none;margin-top:14px;padding:16px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px"> <p style="font-weight:700;color:#166534;margin:0 0 8px">&#10003; 上传成功!</p> <p style="margin:4px 0">文件ID:<code id="kexin-fileid" style="font-size:13px"></code></p> <p style="margin:4px 0">短代码:<input id="kexin-shortcode" readonly onclick="this.select()" style="width:320px;font-family:monospace"></p> <p style="color:#888;font-size:12px;margin:8px 0 0" id="kexin-proc-note"></p> <script>document.getElementById('kexin-proc-note').textContent=kexinVodCfg.procedure?'已触发任务流:'+kexinVodCfg.procedure+',转码+封面处理中...':'未配置任务流,视频不会自动转码。请在系统设置中配置。'</script> </div> <div id="kexin-error" style="display:none;margin-top:14px;padding:14px;background:#fef2f2;border:1px solid #fecaca;border-radius:6px;color:#dc2626"></div> </div> <script> var kexinVodCfg={restUrl:'<?php echo rest_url("kexin-vod/v1/signature"); ?>',saveUrl:'<?php echo rest_url("kexin-vod/v1/save"); ?>',procedure:'<?php echo esc_js(get_option("kexin_vod_procedure","")); ?>',subAppId:'<?php echo esc_js(get_option("kexin_vod_sub_app_id","1500036857")); ?>',storageRegion:'ap-guangzhou'}; function kexinVodInit(){ if(typeof TcVod==='undefined'){setTimeout(kexinVodInit,300);return} var dz=document.getElementById('kexin-dropzone'),fi=document.getElementById('kexin-file-input'), nw=document.getElementById('kexin-name-wrap'),ni=document.getElementById('kexin-vname'), pw=document.getElementById('kexin-progress'),pb=document.getElementById('kexin-bar'), st=document.getElementById('kexin-status'),pt=document.getElementById('kexin-pct'), sp=document.getElementById('kexin-speed'),rw=document.getElementById('kexin-result'), rf=document.getElementById('kexin-fileid'),sc=document.getElementById('kexin-shortcode'), ew=document.getElementById('kexin-error'); dz.onclick=function(){fi.click()}; fi.onchange=function(e){if(e.target.files[0])handle(e.target.files[0])}; dz.ondragover=function(e){e.preventDefault();dz.style.borderColor='#F97316';dz.style.background='#fff8f5'}; dz.ondragleave=function(){dz.style.borderColor='#c0c0c0';dz.style.background='#fafafa'}; dz.ondrop=function(e){e.preventDefault();dz.style.borderColor='#c0c0c0';dz.style.background='#fafafa';if(e.dataTransfer.files[0])handle(e.dataTransfer.files[0])}; function handle(file){ if(!file.type.startsWith('video/')){showErr('请选择视频文件(MP4/MOV/AVI/FLV)');return} nw.style.display='block';if(!ni.value)ni.value=file.name.replace(/\.[^.]+$/,''); ew.style.display='none';pw.style.display='block';rw.style.display='none'; pb.style.width='0%';pt.textContent='0%';st.textContent='正在获取上传签名...';sp.textContent=''; fetch(kexinVodCfg.restUrl,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({mediaType:file.type,mediaName:ni.value||file.name})}) .then(function(r){return r.json()}) .then(function(d){ if(!d.signature)throw new Error(d.message||'签名获取失败'); st.textContent='正在上传...'; var uploader=new TcVod.default({getSignature:function(){return d.signature}}); var task=uploader.upload({mediaFile:file,videoName:ni.value||file.name,procedure:kexinVodCfg.procedure||'',vodSubAppId:kexinVodCfg.subAppId,storageRegion:kexinVodCfg.storageRegion}); task.on('media_progress',function(i){var p=Math.round((i.percent||0)*100);pb.style.width=p+'%';pt.textContent=p+'%';if(i.speed)sp.textContent=(i.speed/1048576).toFixed(1)+' MB/s'}); task.on('media_uploaded',function(){st.textContent='处理中...'}); task.done().then(function(r){ st.textContent='完成!';pb.style.width='100%';pt.textContent='100%'; rf.textContent=r.fileId;sc.value='[kexin_video id=\"'+r.fileId+'\"]';rw.style.display='block'; fetch(kexinVodCfg.saveUrl,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({file_id:r.fileId,name:ni.value||file.name,size:file.size})}).catch(function(){}); }).catch(function(e){showErr('上传失败:'+(e.message||e));pw.style.display='none'}); }).catch(function(e){showErr('签名错误:'+(e.message||e))}); } function showErr(msg){ew.style.display='block';ew.textContent=msg} } kexinVodInit(); </script> </div> <?php } // ====== 视频库页面 ====== function kexin_vod_page_library() { $videos = get_option('kexin_vod_videos', []); $count = count($videos); ?> <div class="wrap"> <h1>视频库 <span style="font-size:14px;color:#888">(共 <?php echo $count; ?> 个视频)</span></h1> <p> <a href="admin.php?page=kexin-vod" class="button button-primary">上传新视频</a> <button class="button" onclick="syncVod()">同步云端</button> <span id="sync-status" style="margin-left:8px;color:#888;display:none">同步中...</span> </p> <?php if (empty($videos)): ?> <div style="text-align:center;padding:60px 20px;background:#fafafa;border-radius:8px;margin-top:20px"> <p style="font-size:1.2rem;color:#888">暂无视频</p> <p><a href="admin.php?page=kexin-vod" class="button button-primary">立即上传</a></p> </div> <?php else: ?> <table class="wp-list-table widefat fixed striped" style="margin-top:12px"> <thead> <tr> <th style="width:50px">序号</th> <th>视频名称</th> <th style="width:200px">文件 ID</th> <th style="width:150px">上传时间</th> <th style="width:160px">短代码</th> <th style="width:160px">操作</th> </tr> </thead> <tbody> <?php $idx = $count; foreach (array_reverse($videos) as $v): $fid = esc_attr($v['file_id'] ?? ''); $name = esc_html($v['name'] ?? '未命名'); $time = esc_html($v['time'] ?? ''); $size = isset($v['size']) ? kexin_vod_format_size($v['size']) : ''; ?> <tr id="vod-row-<?php echo $idx; ?>"> <td><?php echo $idx--; ?></td> <td> <strong><?php echo $name; ?></strong> <?php if ($size) echo '<br><small style="color:#888">' . $size . '</small>'; ?> </td> <td><code style="font-size:12px"><?php echo $fid; ?></code></td> <td><?php echo $time; ?></td> <td> <input value='[kexin_video id="<?php echo $fid; ?>"]' readonly onclick="this.select()" style="width:220px;font-family:monospace;font-size:12px"> </td> <td> <button class="button button-small" onclick="copySC('<?php echo $fid; ?>',this)">复制</button> <button class="button button-small" style="color:#dc2626;border-color:#dc2626" onclick="delVideo('<?php echo $fid; ?>',this)">删除</button> </td> </tr> <?php endforeach; ?> </tbody> </table> <?php endif; ?> </div> <script> function copySC(id,btn){var code='[kexin_video id="'+id+'"]';navigator.clipboard.writeText(code);btn.textContent='已复制!';setTimeout(function(){btn.textContent='复制'},1500)} function delVideo(id,btn){ if(!confirm('确定从本地库中删除此视频记录?\n(云端视频不会被删除,仅移除本地记录)'))return; fetch('<?php echo rest_url("kexin-vod/v1/delete"); ?>',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({file_id:id})}) .then(function(r){return r.json()}) .then(function(d){if(d.success){btn.closest('tr').remove();if(!document.querySelectorAll('tbody tr').length)location.reload()}else{alert('删除失败')}}) .catch(function(){alert('网络错误')}); } function syncVod(){ var st=document.getElementById('sync-status');st.style.display='inline';st.textContent='正在校验云端...'; fetch('<?php echo rest_url("kexin-vod/v1/sync"); ?>',{method:'POST'}) .then(function(r){return r.json()}) .then(function(d){ if(d.success){ st.textContent='同步完成! 有效'+d.valid+'个, 已移除'+d.removed+'个'; if(d.removed>0){setTimeout(function(){location.reload()},1500)} }else{st.textContent='同步失败: '+(d.message||'')} }).catch(function(){st.textContent='网络错误'}); } </script> <?php } function kexin_vod_format_size($bytes) { if ($bytes >= 1073741824) return round($bytes / 1073741824, 1) . ' GB'; if ($bytes >= 1048576) return round($bytes / 1048576, 1) . ' MB'; if ($bytes >= 1024) return round($bytes / 1024, 1) . ' KB'; return $bytes . ' B'; } // ====== 设置页面 ====== function kexin_vod_page_settings() { if (isset($_POST['kexin_save'])) { update_option('kexin_vod_secret_id', sanitize_text_field($_POST['secret_id'])); update_option('kexin_vod_secret_key', sanitize_text_field($_POST['secret_key'])); update_option('kexin_vod_sub_app_id', sanitize_text_field($_POST['sub_app_id'])); update_option('kexin_vod_app_id', sanitize_text_field($_POST['app_id'])); update_option('kexin_vod_pkey', sanitize_text_field($_POST['pkey'])); update_option('kexin_vod_procedure', sanitize_text_field($_POST['procedure'])); echo '<div class="notice notice-success is-dismissible"><p>设置已保存。</p></div>'; } ?> <div class="wrap"> <h1>系统设置</h1> <form method="post"> <table class="form-table"> <tr> <th scope="row">SecretId</th> <td><input name="secret_id" value="<?php echo esc_attr(get_option('kexin_vod_secret_id','')); ?>" class="regular-text" placeholder="AKIDxxxxxxxx"> <p class="description">腾讯云 API 密钥 SecretId</p></td> </tr> <tr> <th scope="row">SecretKey</th> <td><input name="secret_key" value="<?php echo esc_attr(get_option('kexin_vod_secret_key','')); ?>" class="regular-text" type="password" placeholder="••••••••"> <p class="description">腾讯云 API 密钥 SecretKey</p></td> </tr> <tr> <th scope="row">VOD AppID</th> <td><input name="app_id" value="<?php echo esc_attr(get_option('kexin_vod_app_id','1500036857')); ?>" class="regular-text"> <p class="description">云点播 AppID,在 <a href="https://console.cloud.tencent.com/developer" target="_blank">账号信息</a> 查看(10位数字)</p></td> </tr> <tr> <th scope="row">子应用 ID</th> <td><input name="sub_app_id" value="<?php echo esc_attr(get_option('kexin_vod_sub_app_id','0')); ?>" class="regular-text"> <p class="description">上传签名用,与 AppID 通常相同</p></td> </tr> <tr> <th scope="row">播放密钥 (pkey)</th> <td><input name="pkey" value="<?php echo esc_attr(get_option('kexin_vod_pkey','')); ?>" class="regular-text" type="password"> <p class="description">云点播控制台 → 分发播放设置 → 默认分发配置 → 播放密钥。psign 签名必需。</p></td> </tr> <tr> <th scope="row">任务流名称</th> <td><input name="procedure" value="<?php echo esc_attr(get_option('kexin_vod_procedure','')); ?>" class="regular-text" placeholder="例如:psste_com_video"> <p class="description">腾讯云点播控制台 → 任务流设置 → 创建含「自适应转码 + 封面截图」的任务流</p></td> </tr> </table> <p class="submit"><button type="submit" name="kexin_save" class="button button-primary">保存设置</button></p> </form> <hr style="margin:32px 0"> <h2>使用说明</h2> <ol style="line-height:2"> <li>在 <a href="https://console.cloud.tencent.com/cam/capi" target="_blank">腾讯云访问密钥</a> 获取 SecretId 和 SecretKey</li> <li>在 <a href="https://console.cloud.tencent.com/vod" target="_blank">云点播控制台</a> 创建<strong>任务流</strong>,包含「自适应码流转码」和「封面截图」</li> <li>上传视频 → 自动转码 → 在视频库复制短代码</li> <li>将短代码 <code>[kexin_video id="文件ID"]</code> 粘贴到任意页面或文章</li> <li>TCPlayer 自动根据用户网速选择最佳清晰度,用户也可手动切换</li> </ol> <h3>短代码参数</h3> <table class="widefat" style="max-width:500px"> <tr><td><code>[kexin_video id="xxx"]</code></td><td>自适应宽度</td></tr> <tr><td><code>[kexin_video id="xxx" width="800px"]</code></td><td>固定宽度</td></tr> <tr><td><code>[kexin_video id="xxx" psign="签名"]</code></td><td>开启播放防盗链</td></tr> </table> </div> <?php } // ====== REST API ====== add_action('rest_api_init', function() { // 上传签名 register_rest_route('kexin-vod/v1', '/signature', [ 'methods' => 'POST', 'callback' => function($r) { $sid = get_option('kexin_vod_secret_id', ''); $skey = get_option('kexin_vod_secret_key', ''); $sub = get_option('kexin_vod_sub_app_id', '1500036857'); if (empty($sid) || empty($skey)) { return new WP_Error('no_keys', '请先配置腾讯云密钥', ['status' => 500]); } $now = time(); $exp = $now + 7200; $proc = get_option('kexin_vod_procedure', ''); $params = [ 'secretId' => $sid, 'currentTimeStamp' => $now, 'expireTime' => $exp, 'random' => rand(0, 4294967295), 'classId' => 0, 'oneTimeValid' => 0, 'vodSubAppId' => $sub, ]; if (!empty($proc)) $params['procedure'] = $proc; $original = http_build_query($params); $hmacBin = hash_hmac('sha1', $original, $skey, true); $signature = base64_encode($hmacBin . $original); return ['success' => true, 'signature' => $signature, 'vodSubAppId' => $sub]; }, 'permission_callback' => '__return_true', ]); // 保存视频记录 register_rest_route('kexin-vod/v1', '/save', [ 'methods' => 'POST', 'callback' => function($r) { $videos = get_option('kexin_vod_videos', []); $videos[] = [ 'file_id' => sanitize_text_field($r->get_param('file_id')), 'name' => sanitize_text_field($r->get_param('name')), 'size' => intval($r->get_param('size')), 'time' => current_time('mysql'), ]; update_option('kexin_vod_videos', $videos); return ['success' => true, 'count' => count($videos)]; }, 'permission_callback' => function() { return current_user_can('edit_posts'); }, ]); // 列出视频 register_rest_route('kexin-vod/v1', '/list', [ 'methods' => 'GET', 'callback' => function() { return ['success' => true, 'videos' => get_option('kexin_vod_videos', [])]; }, 'permission_callback' => '__return_true', ]); // 同步云端视频(安全模式:只标记,不自动删除) register_rest_route('kexin-vod/v1', '/sync', [ 'methods' => 'POST', 'callback' => function() { $sid = get_option('kexin_vod_secret_id', ''); $skey = get_option('kexin_vod_secret_key', ''); $sub = get_option('kexin_vod_sub_app_id', '1500036857'); $videos = get_option('kexin_vod_videos', []); if (empty($sid) || empty($skey)) { return ['success' => false, 'message' => '请先配置密钥']; } if (empty($videos)) return ['success' => true, 'valid' => 0, 'removed' => 0, 'message' => '本地无记录']; // Collect all file IDs $fileIds = array_map(function($v) { return $v['file_id']; }, $videos); // Check each file ID against VOD API $validIds = []; foreach (array_chunk($fileIds, 20) as $chunk) { $result = kexin_vod_api_call($sid, $skey, $sub, 'DescribeMediaInfos', ['FileIds' => $chunk]); if ($result && isset($result['MediaInfoSet']) && !isset($result['Error'])) { foreach ($result['MediaInfoSet'] as $media) { $validIds[] = $media['FileId']; } } // If API fails for a chunk, skip that chunk (don't delete its records) } // SAFETY: Only remove records that were CONFIRMED non-existent by a SUCCESSFUL API call // If no API calls succeeded (validIds empty but we had files), don't touch anything $checkedCount = count($validIds); if ($checkedCount === 0 && empty($videos)) { return ['success' => true, 'valid' => 0, 'removed' => 0, 'message' => '本地无记录']; } if ($checkedCount === 0) { return ['success' => true, 'valid' => 0, 'removed' => 0, 'missing' => count($fileIds), 'message' => '校验完成:本地记录有效0个,云端文件可能已被删除']; } $removed = 0; $newVideos = []; foreach ($videos as $v) { if (in_array($v['file_id'], $validIds) || !in_array($v['file_id'], $fileIds)) { // Keep if: confirmed valid OR wasn't checked $newVideos[] = $v; } else { // Only remove if: was checked AND was NOT found in valid set // But wait - we can't distinguish "checked and not found" from "not checked" // So we only keep records. Manual delete is safer. $newVideos[] = $v; } } // Actually: NEVER auto-delete. Just report status. $missing = array_diff($fileIds, $validIds); return [ 'success' => true, 'valid' => $checkedCount, 'removed' => 0, 'missing' => count($missing), 'message' => '云端有效 ' . $checkedCount . ' 个,本地 ' . count($videos) . ' 个。用删除按钮手动清理。' ]; }, 'permission_callback' => '__return_true', ]); // 调试:返回生成的psign register_rest_route('kexin-vod/v1', '/debug-psign', [ 'methods' => 'GET', 'callback' => function() { $pkey = get_option('kexin_vod_pkey', 'NOT SET'); $app = get_option('kexin_vod_app_id', '1500036857'); $fid = '5001834805555162521'; // official test file $psign = kexin_vod_player_sign($fid, $app, $pkey); return [ 'pkey' => $pkey, 'pkey_len' => strlen($pkey), 'appId' => $app, 'fileId' => $fid, 'psign' => $psign, 'official_psign' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBJZCI6MTUwMDAzNjg1NywiZmlsZUlkIjoiNTAwMTgzNDgwNTU1NTE2MjUyMSIsImN1cnJlbnRUaW1lU3RhbXAiOjE3ODAxNDYyMzMsImNvbnRlbnRJbmZvIjp7ImF1ZGlvVmlkZW9UeXBlIjoiT3JpZ2luYWwiLCJpbWFnZVNwcml0ZURlZmluaXRpb24iOjEwfSwidXJsQWNjZXNzSW5mbyI6eyJkb21haW4iOiJ2LnBzc3RlLmNvbSIsInNjaGVtZSI6IkhUVFBTIn19.a3ButwrP9s-O-uo6zuYyN1jVyimIxQjYRCML73-QZ2Y', 'header_match' => (explode('.', $psign)[0] ?? '') === 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', ]; }, 'permission_callback' => '__return_true', ]); // 删除视频记录 register_rest_route('kexin-vod/v1', '/delete', [ 'methods' => 'POST', 'callback' => function($r) { $fid = $r->get_param('file_id'); $videos = get_option('kexin_vod_videos', []); $videos = array_filter($videos, function($v) use ($fid) { return ($v['file_id'] ?? '') !== $fid; }); update_option('kexin_vod_videos', array_values($videos)); return ['success' => true]; }, 'permission_callback' => function() { return current_user_can('edit_posts'); }, ]); }); // ====== 腾讯云 API 调用 (TC3 签名) ====== function kexin_vod_api_call($secretId, $secretKey, $subAppId, $action, $params) { $service = 'vod'; $host = 'vod.tencentcloudapi.com'; $version = '2018-07-17'; $region = 'ap-guangzhou'; $timestamp = time(); $date = gmdate('Y-m-d', $timestamp); $payload = json_encode($params); // TC3-HMAC-SHA256 signing $canonicalHeaders = "content-type:application/json\nhost:{$host}\n"; $signedHeaders = 'content-type;host'; $hashedPayload = hash('sha256', $payload); $canonicalRequest = "POST\n/\n\n{$canonicalHeaders}\n{$signedHeaders}\n{$hashedPayload}"; $algorithm = 'TC3-HMAC-SHA256'; $credentialScope = "{$date}/{$service}/tc3_request"; $stringToSign = "{$algorithm}\n{$timestamp}\n{$credentialScope}\n" . hash('sha256', $canonicalRequest); $secretDate = hash_hmac('sha256', $date, 'TC3' . $secretKey, true); $secretService = hash_hmac('sha256', $service, $secretDate, true); $secretSigning = hash_hmac('sha256', 'tc3_request', $secretService, true); $signature = hash_hmac('sha256', $stringToSign, $secretSigning); $authorization = "{$algorithm} Credential={$secretId}/{$credentialScope}, SignedHeaders={$signedHeaders}, Signature={$signature}"; $headers = [ 'Authorization' => $authorization, 'Content-Type' => 'application/json', 'Host' => $host, 'X-TC-Action' => $action, 'X-TC-Version' => $version, 'X-TC-Timestamp' => (string)$timestamp, 'X-TC-Region' => $region, ]; if (!empty($subAppId) && $subAppId !== '0') { $headers['X-TC-SubAppId'] = $subAppId; } $response = wp_remote_post("https://{$host}", [ 'headers' => $headers, 'body' => $payload, 'timeout' => 20, 'sslverify' => true, ]); if (is_wp_error($response)) { return ['Error' => ['Message' => 'HTTP Error: ' . $response->get_error_message()]]; } $body = wp_remote_retrieve_body($response); $code = wp_remote_retrieve_response_code($response); $data = json_decode($body, true); if ($code !== 200 || !$data) { return ['Error' => ['Message' => "HTTP {$code}: " . substr($body, 0, 200)]]; } if (isset($data['Response']['Error'])) { return ['Error' => $data['Response']['Error']]; } return $data['Response'] ?? ['Error' => ['Message' => 'Empty response']]; } // ====== 播放签名 psign ====== function kexin_vod_player_sign($fileId, $appId, $pkey) { $now = time(); $header = ['alg' => 'HS256', 'typ' => 'JWT']; $payload = [ 'appId' => intval($appId), 'fileId' => (string)$fileId, 'currentTimeStamp' => $now, 'contentInfo' => ['audioVideoType' => 'Original', 'imageSpriteDefinition' => 10], 'urlAccessInfo' => ['domain' => 'v.psste.com', 'scheme' => 'HTTPS'], ]; $b64h = rtrim(strtr(base64_encode(json_encode($header)), '+/', '-_'), '='); $b64p = rtrim(strtr(base64_encode(json_encode($payload, JSON_UNESCAPED_SLASHES)), '+/', '-_'), '='); $sign = rtrim(strtr(base64_encode(hash_hmac('sha256', "$b64h.$b64p", $pkey, true)), '+/', '-_'), '='); return "$b64h.$b64p.$sign"; } // ====== 短代码:TCPlayer ====== add_shortcode('kexin_video', function($atts) { $a = shortcode_atts(['id' => '', 'width' => '100%', 'psign' => ''], $atts); if (empty($a['id'])) return '<p style="color:#dc2626">请指定视频 FileID</p>'; $fid = esc_attr($a['id']); $app = esc_attr(get_option('kexin_vod_app_id', '1500036857')); $pkey = get_option('kexin_vod_pkey', ''); // 自动生成播放签名 $psign_value = ''; if (!empty($a['psign'])) { $psign_value = esc_attr($a['psign']); } elseif (!empty($pkey)) { $psign_value = kexin_vod_player_sign($fid, $app, $pkey); } $pid = 'tcplayer_' . uniqid(); ob_start(); ?> <video id="<?php echo $pid; ?>" preload="auto" playsinline webkit-playsinline x5-playsinline style="width:100%;max-width:<?php echo esc_attr($a['width']); ?>;aspect-ratio:16/9;background:#000;border-radius:8px"></video> <script> (function() { var pid = '<?php echo $pid; ?>'; var fid = '<?php echo $fid; ?>'; var app = '<?php echo $app; ?>'; function initPlayer() { var opts = { fileID: fid, appID: app, autoplay: false }; var psignVal = '<?php echo $psign_value; ?>'; if (psignVal) opts.psign = psignVal; if (typeof TCPlayer !== 'undefined') { TCPlayer(pid, opts); } else { (window.TCPlayerInitQueue = window.TCPlayerInitQueue || []).push(function() { TCPlayer(pid, opts); }); var retryTimer = setInterval(function() { if (typeof TCPlayer !== 'undefined') { clearInterval(retryTimer); var queue = window.TCPlayerInitQueue || []; while (queue.length) { (queue.shift())(); } window.TCPlayerInitQueue = []; } }, 300); setTimeout(function() { clearInterval(retryTimer); }, 15000); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initPlayer); } else { initPlayer(); } })(); </script> <?php return ob_get_clean(); }); // ====== 前端加载 TCPlayer ====== add_action('wp_enqueue_scripts', function() { wp_enqueue_style('tcplayer-css', 'https://tcsdk.com/player/tcplayer/release/v5.3.4/tcplayer.min.css', [], KEXIN_VOD_VER); wp_enqueue_script('tcplayer-js', 'https://tcsdk.com/player/tcplayer/release/v5.3.4/tcplayer.v5.3.4.min.js', [], KEXIN_VOD_VER, false); }); 油漆涂料 – 第 32 页 – 涂料工厂|油漆厂家

油漆涂料

共 503 篇文章 · 分类:油漆涂料

全部 油漆涂料 (500) 技术知识 (14) 行业动态 (9) 应用案例 (7) 企业新闻 (3)
1 30 31 32 33 34 42