367 lines
12 KiB
Go
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>`
|