2026-05-12 19:34:47 +08:00

367 lines
12 KiB
Go

package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
"gopkg.in/yaml.v3"
"hyapp/pkg/tencentrtc"
)
type fileConfig struct {
TencentRTC rtcYAMLConfig `yaml:"tencent_rtc"`
}
type rtcYAMLConfig struct {
Enabled bool `yaml:"enabled"`
SDKAppID int64 `yaml:"sdk_app_id"`
SecretKey string `yaml:"secret_key"`
UserSigTTL time.Duration `yaml:"user_sig_ttl"`
RoomIDType string `yaml:"room_id_type"`
AppScene string `yaml:"app_scene"`
}
type server struct {
rtcConfig tencentrtc.TokenConfig
}
func main() {
configPath := flag.String("config", "services/gateway-service/configs/config.yaml", "gateway config containing tencent_rtc")
addr := flag.String("addr", "127.0.0.1:13100", "listen address")
flag.Parse()
rtcConfig, err := loadRTCConfig(*configPath)
if err != nil {
log.Fatalf("load rtc config failed: %v", err)
}
if err := tencentrtc.ValidateConfig(rtcConfig); err != nil {
log.Fatalf("rtc config is not usable: %v", err)
}
s := &server{rtcConfig: rtcConfig}
mux := http.NewServeMux()
mux.HandleFunc("/", s.index)
mux.HandleFunc("/api/token", s.token)
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("ok"))
})
log.Printf("TRTC smoke demo listening on http://%s", *addr)
log.Printf("SDKAppID=%d room_id_type=%s app_scene=%s", rtcConfig.SDKAppID, rtcConfig.RoomIDType, rtcConfig.AppScene)
if err := http.ListenAndServe(*addr, mux); err != nil {
log.Fatal(err)
}
}
func loadRTCConfig(path string) (tencentrtc.TokenConfig, error) {
content, err := os.ReadFile(path)
if err != nil {
return tencentrtc.TokenConfig{}, err
}
var cfg fileConfig
if err := yaml.Unmarshal(content, &cfg); err != nil {
return tencentrtc.TokenConfig{}, err
}
if cfg.TencentRTC.UserSigTTL <= 0 {
return tencentrtc.TokenConfig{}, errors.New("tencent_rtc.user_sig_ttl is required")
}
return tencentrtc.TokenConfig{
Enabled: cfg.TencentRTC.Enabled,
SDKAppID: cfg.TencentRTC.SDKAppID,
SecretKey: strings.TrimSpace(cfg.TencentRTC.SecretKey),
TTL: cfg.TencentRTC.UserSigTTL,
RoomIDType: strings.TrimSpace(cfg.TencentRTC.RoomIDType),
AppScene: strings.TrimSpace(cfg.TencentRTC.AppScene),
}, nil
}
func (s *server) index(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
http.ServeContent(w, r, "index.html", time.Now(), strings.NewReader(indexHTML))
}
func (s *server) token(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if r.Method != http.MethodGet {
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
return
}
userID, err := parseUserID(r.URL.Query().Get("user_id"))
if err != nil {
writeJSONError(w, http.StatusBadRequest, err)
return
}
roomID := strings.TrimSpace(r.URL.Query().Get("room_id"))
if roomID == "" {
writeJSONError(w, http.StatusBadRequest, errors.New("room_id is required"))
return
}
token, err := tencentrtc.GenerateToken(s.rtcConfig, userID, roomID, time.Now().UTC())
if err != nil {
writeJSONError(w, http.StatusBadRequest, err)
return
}
_ = json.NewEncoder(w).Encode(token)
}
func parseUserID(raw string) (int64, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return 0, errors.New("user_id is required")
}
value, err := strconv.ParseInt(raw, 10, 64)
if err != nil || value <= 0 {
return 0, fmt.Errorf("user_id must be a positive int64")
}
return value, nil
}
func writeJSONError(w http.ResponseWriter, status int, err error) {
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
}
const indexHTML = `<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>TRTC Smoke</title>
<script src="https://web.sdk.qcloud.com/trtc/webrtc/v5/dist/trtc.js"></script>
<style>
:root { color-scheme: light; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
body { margin: 0; background: #f6f8fb; color: #151922; }
main { max-width: 920px; margin: 0 auto; padding: 28px; }
h1 { margin: 0 0 20px; font-size: 24px; }
section { background: #fff; border: 1px solid #e4e8f0; border-radius: 8px; padding: 18px; margin-bottom: 16px; }
label { display: block; font-size: 13px; color: #526070; margin-bottom: 6px; }
input, select { width: 100%; box-sizing: border-box; padding: 10px 12px; border: 1px solid #cfd6e4; border-radius: 6px; font-size: 14px; }
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
.actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 14px; }
button { border: 1px solid #2f6df6; background: #2f6df6; color: #fff; padding: 10px 14px; border-radius: 6px; font-size: 14px; cursor: pointer; }
button.secondary { background: #fff; color: #2f6df6; }
button:disabled { opacity: .55; cursor: not-allowed; }
pre { min-height: 260px; max-height: 420px; overflow: auto; margin: 0; padding: 14px; background: #0d1117; color: #d8dee9; border-radius: 8px; font-size: 12px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
.status { font-size: 14px; margin: 0 0 10px; }
.ok { color: #0b7a36; }
.bad { color: #b42318; }
@media (max-width: 720px) { main { padding: 18px; } .grid { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<main>
<h1>TRTC Smoke</h1>
<section>
<div class="grid">
<div>
<label for="userId">user_id / rtc_user_id</label>
<input id="userId" value="123456" autocomplete="off" />
</div>
<div>
<label for="roomId">str_room_id</label>
<input id="roomId" autocomplete="off" />
</div>
<div>
<label for="scene">scene</label>
<select id="scene">
<option value="live" selected>live + audience</option>
<option value="rtc">rtc</option>
</select>
</div>
<div>
<label for="role">role</label>
<select id="role">
<option value="audience" selected>audience</option>
<option value="anchor">anchor</option>
</select>
</div>
</div>
<div class="actions">
<button id="enterBtn">Enter Room</button>
<button id="exitBtn" class="secondary" disabled>Exit Room</button>
<button id="audioBtn" class="secondary" disabled>Start Local Audio</button>
<button id="clearBtn" class="secondary">Clear Log</button>
</div>
</section>
<section>
<p id="status" class="status">Ready</p>
<pre id="log"></pre>
</section>
</main>
<script>
const $ = id => document.getElementById(id);
const state = { trtc: null, inRoom: false, audio: false };
function defaultRoomId() {
const suffix = Math.random().toString(16).slice(2, 10);
return "trtc_smoke_" + suffix;
}
function appendLog(message, data) {
const ts = new Date().toISOString();
const line = data === undefined ? message : message + " " + safeStringify(data);
$("log").textContent += "[" + ts + "] " + line + "\n";
$("log").scrollTop = $("log").scrollHeight;
}
function safeStringify(value) {
try {
if (value instanceof Error) {
const errorData = { name: value.name, message: value.message, stack: value.stack };
for (const key of Object.keys(value)) errorData[key] = value[key];
return JSON.stringify(errorData);
}
return JSON.stringify(value);
} catch (_) {
return String(value);
}
}
function setStatus(text, ok) {
$("status").textContent = text;
$("status").className = "status " + (ok ? "ok" : "bad");
}
function updateButtons() {
$("enterBtn").disabled = state.inRoom;
$("exitBtn").disabled = !state.inRoom;
$("audioBtn").disabled = !state.inRoom || $("role").value !== "anchor";
$("audioBtn").textContent = state.audio ? "Stop Local Audio" : "Start Local Audio";
}
async function fetchToken() {
const params = new URLSearchParams({
user_id: $("userId").value.trim(),
room_id: $("roomId").value.trim()
});
const resp = await fetch("/api/token?" + params.toString());
const data = await resp.json();
if (!resp.ok) throw new Error(data.error || "token request failed");
return data;
}
function bindEvents(trtc) {
const events = TRTC.EVENT || {};
for (const key of ["ERROR", "KICKED_OUT", "REMOTE_USER_ENTER", "REMOTE_USER_EXIT", "REMOTE_AUDIO_AVAILABLE", "REMOTE_AUDIO_UNAVAILABLE", "NETWORK_QUALITY"]) {
if (events[key]) {
trtc.on(events[key], event => appendLog("event." + key, event));
}
}
}
async function enterRoom() {
try {
setStatus("Requesting token...", true);
appendLog("browser", { userAgent: navigator.userAgent });
if (TRTC.isSupported) {
appendLog("isSupported", await TRTC.isSupported());
}
const token = await fetchToken();
appendLog("token", {
sdk_app_id: token.sdk_app_id,
rtc_user_id: token.rtc_user_id,
str_room_id: token.str_room_id,
expire_at_ms: token.expire_at_ms,
role: token.role,
app_scene: token.app_scene
});
state.trtc = TRTC.create();
bindEvents(state.trtc);
const roomOptions = {
sdkAppId: token.sdk_app_id,
userId: token.rtc_user_id,
userSig: token.user_sig,
strRoomId: token.str_room_id
};
if ($("scene").value === "live") {
roomOptions.scene = TRTC.TYPE.SCENE_LIVE;
roomOptions.role = $("role").value === "anchor" ? TRTC.TYPE.ROLE_ANCHOR : TRTC.TYPE.ROLE_AUDIENCE;
} else {
roomOptions.scene = TRTC.TYPE.SCENE_RTC;
}
appendLog("enterRoom.options", {
sdkAppId: roomOptions.sdkAppId,
userId: roomOptions.userId,
strRoomId: roomOptions.strRoomId,
scene: roomOptions.scene,
role: roomOptions.role
});
setStatus("Entering room...", true);
await state.trtc.enterRoom(roomOptions);
state.inRoom = true;
setStatus("Entered room", true);
appendLog("enterRoom.success");
} catch (err) {
setStatus("Enter failed", false);
appendLog("enterRoom.error", err);
if (state.trtc) {
try { await state.trtc.destroy(); } catch (_) {}
state.trtc = null;
}
} finally {
updateButtons();
}
}
async function exitRoom() {
try {
if (state.audio) await toggleAudio();
if (state.trtc) {
await state.trtc.exitRoom();
await state.trtc.destroy();
}
appendLog("exitRoom.success");
setStatus("Exited room", true);
} catch (err) {
setStatus("Exit failed", false);
appendLog("exitRoom.error", err);
} finally {
state.trtc = null;
state.inRoom = false;
state.audio = false;
updateButtons();
}
}
async function toggleAudio() {
if (!state.trtc) return;
if (!state.audio) {
await state.trtc.startLocalAudio();
state.audio = true;
appendLog("startLocalAudio.success");
} else {
await state.trtc.stopLocalAudio();
state.audio = false;
appendLog("stopLocalAudio.success");
}
updateButtons();
}
$("roomId").value = defaultRoomId();
$("enterBtn").addEventListener("click", enterRoom);
$("exitBtn").addEventListener("click", exitRoom);
$("audioBtn").addEventListener("click", () => toggleAudio().catch(err => appendLog("audio.error", err)));
$("clearBtn").addEventListener("click", () => { $("log").textContent = ""; });
$("role").addEventListener("change", updateButtons);
updateButtons();
</script>
</body>
</html>`