Files
zyt/server/app/api/controller/TrtcController.php
T
2026-03-24 16:32:56 +08:00

201 lines
6.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// +----------------------------------------------------------------------
// | 腾讯云 TRTC 云端录制 HTTP 回调(需在控制台填写本接口完整 URL)
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace app\api\controller;
use app\common\model\tcm\CallRecord;
use app\common\service\FileService;
use think\facade\Log;
class TrtcController extends BaseApiController
{
/** 免登录:腾讯云服务器回调 */
public array $notNeedLogin = ['recordingNotify'];
/**
* POST /api/trtc/recording-notify
* 可选安全:环境变量 TRTC_RECORDING_CALLBACK_TOKEN 非空时,Query 需带 ?token=xxx
*/
public function recordingNotify()
{
$token = (string)config('trtc.recording_callback_token', '');
if ($token !== '') {
$q = (string)($this->request->param('token', ''));
if (!hash_equals($token, $q)) {
return json(['code' => -1, 'msg' => 'forbidden']);
}
}
$raw = $this->request->getContent();
$json = json_decode($raw, true);
if (!is_array($json)) {
Log::warning('TRTC recording callback: invalid json', ['raw' => substr($raw, 0, 500)]);
return json(['code' => 0, 'msg' => 'ok']);
}
$roomId = $this->extractRoomId($json);
$urls = $this->extractRecordingUrls($json);
if ($roomId === '' || $urls === []) {
Log::info('TRTC recording callback: skip (no room or no urls)', [
'roomId' => $roomId,
'eventType' => $json['EventType'] ?? null,
]);
return json(['code' => 0, 'msg' => 'ok']);
}
$record = CallRecord::where('room_id', $roomId)
->order('id', 'desc')
->find();
if (!$record) {
// 兼容数字房间号与字符串
$record = CallRecord::where('room_id', (string)(int)$roomId)
->order('id', 'desc')
->find();
}
if (!$record) {
Log::warning('TRTC recording: no call_record for room', ['roomId' => $roomId]);
return json(['code' => 0, 'msg' => 'ok']);
}
$prev = [];
if (!empty($record->recording_urls)) {
$prev = json_decode((string)$record->recording_urls, true);
if (!is_array($prev)) {
$prev = [];
}
}
$merged = array_values(array_unique(array_merge($prev, $urls)));
$merged = $this->mirrorRecordingUrlsIfEnabled($merged);
$record->save([
'recording_urls' => json_encode($merged, JSON_UNESCAPED_UNICODE),
'recording_status' => 2,
'update_time' => time(),
]);
Log::info('TRTC recording saved', ['id' => $record->id, 'urls' => count($merged)]);
return json(['code' => 0, 'msg' => 'ok']);
}
private function extractRoomId(array $data): string
{
$candidates = [
data_get($data, 'EventInfo.RoomId'),
data_get($data, 'EventInfo.RoomIdStr'),
data_get($data, 'RoomId'),
data_get($data, 'room_id'),
];
foreach ($candidates as $v) {
if ($v !== null && $v !== '') {
return trim((string)$v);
}
}
return '';
}
/**
* 从回调 JSON 中提取录制文件地址(混流/单路字段名在不同版本可能不同)
*/
private function extractRecordingUrls(array $data): array
{
$urls = [];
$this->walkForUrls($data, $urls);
return array_values(array_unique(array_filter($urls)));
}
private function walkForUrls($node, array &$urls): void
{
if (!is_array($node)) {
return;
}
foreach ($node as $key => $val) {
if (is_string($key) && in_array($key, ['VideoUrl', 'FileUrl', 'MediaUrl', 'Url', 'url'], true) && is_string($val)) {
$v = trim($val);
if ($v !== '' && str_starts_with($v, 'http')) {
$urls[] = $v;
}
}
if (is_array($val)) {
$this->walkForUrls($val, $urls);
}
}
}
/**
* 将腾讯云录制地址拉取到本服务器 public/uploads(需在 .env 开启 trtc.recording_mirror_to_storage=1
* @param array<int, string> $urls
* @return array<int, string>
*/
private function mirrorRecordingUrlsIfEnabled(array $urls): array
{
if (!(int)config('trtc.recording_mirror_to_storage', 0)) {
return $urls;
}
$out = [];
foreach ($urls as $u) {
$u = trim((string)$u);
if ($u === '' || strncmp($u, 'http', 4) !== 0) {
$out[] = $u;
continue;
}
$local = $this->downloadRecordingToPublic($u);
$out[] = $local !== null ? FileService::getFileUrl($local) : $u;
}
return $out;
}
private function downloadRecordingToPublic(string $url): ?string
{
$dirRel = 'uploads/video/trtc_recording/' . date('Ymd');
$dirAbs = public_path() . $dirRel;
if (!is_dir($dirAbs) && !mkdir($dirAbs, 0755, true) && !is_dir($dirAbs)) {
Log::warning('TRTC mirror: mkdir failed', ['dir' => $dirAbs]);
return null;
}
$ext = '.mp4';
if (preg_match('/\.([a-z0-9]+)(\?|#|$)/i', $url, $m)) {
$ext = '.' . strtolower($m[1]);
}
$name = 'trtc_' . date('His') . '_' . bin2hex(random_bytes(4)) . $ext;
$target = $dirAbs . DIRECTORY_SEPARATOR . $name;
$ctx = stream_context_create([
'http' => ['timeout' => 600],
'ssl' => ['verify_peer' => true, 'verify_peer_name' => true],
]);
try {
$src = @fopen($url, 'rb', false, $ctx);
if ($src === false) {
return null;
}
$dst = @fopen($target, 'wb');
if ($dst === false) {
fclose($src);
return null;
}
stream_copy_to_stream($src, $dst);
fclose($src);
fclose($dst);
} catch (\Throwable $e) {
Log::warning('TRTC mirror download failed: ' . $e->getMessage());
return null;
}
if (!is_file($target) || filesize($target) < 1) {
return null;
}
return str_replace('\\', '/', $dirRel . '/' . $name);
}
}