import {
ISuperNodeWebbrtc,
IRTCOpstion,
IUrlParams,
IStreams,
IRtcdnDraft,
IParmUrl,
AjaxResponse,
} from "@/typings/interface";
/* eslint-disable @typescript-eslint/no-explicit-any */
declare global {
interface HTMLVideoElement {
// eslint-disable-next-line @typescript-eslint/method-signature-style
captureStream(frameRate?: number): MediaStream;
}
}
/**
* 兼容 ts 无法识别
*/
declare let MediaRecorder: any;
type MediaRecorders = InstanceType<typeof MediaRecorder>;
// const recordedBlobsArrs: any = []; // 视频录制流
/**
* 播放器类
*/
export default class SUPERNODETSWEBRTC implements ISuperNodeWebbrtc {
public elVideo: HTMLVideoElement; // 播放器 标签1
public pc: RTCPeerConnection | null = null; // rtc pc 播放对象
public opstion: IRTCOpstion; // 选项
public mediaRecorder: MediaRecorders | null = null; // 录制视频
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected url: string; // 传入参数
protected urlParams: IUrlParams; // url参数
public recordedBlobsArrs: any = [];
public constructor(
url: string,
elVideo: HTMLVideoElement,
opstion?: IRTCOpstion
) {
// 校验url地址是否正确
if (!url.match(/^webrtc?:\/\//)) {
throw "播放地址不正确 webrtc://xxxxxx";
}
this.url = url; // 用户输入url地址
this.elVideo = elVideo; // video 标签
this.opstion = opstion || {}; // rtc 服务器选项
this.urlParams = this.ParseUrl(); // 拆解url参数
}
// 开始
public Play(): Promise<RTCPeerConnection> {
return new Promise((res, rej) => {
this.pc = null;
// 是否自动播放
if (this.opstion.autoplay) {
this.elVideo.setAttribute("autoplay", "autoplay");
}
this.StartLoading();
if (this.pc) {
res(this.pc);
} else {
rej(false);
}
});
}
// 暂停
public Pause(): void {
this.elVideo.pause();
}
// 暂停恢复
public Resume(): void {
this.elVideo.play();
}
public Stop(): void {
console.log("stop");
}
// 停止播放并清理相关的播放资源(销毁)
public Destroy(): void {
this.Pause();
if (this.pc) {
this.pc.close();
this.pc = null;
}
}
// 只读,是否暂停播放
public Paused(): void {
console.log("Paused");
}
// 视频录制 webApi https://developer.mozilla.org/zh-CN/docs/Web/API/MediaStream_Recording_API
public async RecordingStart(
coding = "video/webm; codecs=vp9"
): Promise<void> {
const stream = this.elVideo.captureStream();
this.mediaRecorder = new MediaRecorder(stream, { mimeType: coding });
this.mediaRecorder.ondataavailable = (event: any) => {
console.log("录制成功:", event);
if (event.data && event.data.size > 0) {
this.recordedBlobsArrs.push(event.data);
}
};
this.mediaRecorder.start();
}
// 视频录制播放
public RecordingPaly(recordedVideo: HTMLVideoElement): void {
const superBuffer = new Blob(this.recordedBlobsArrs, {
type: "video/webm; codecs=vp9",
});
recordedVideo.src = "";
recordedVideo.srcObject = null;
recordedVideo.src = window.URL.createObjectURL(superBuffer);
recordedVideo.controls = true;
recordedVideo.setAttribute("autoplay", "autoplay");
recordedVideo.play();
}
// 视频停止录制
public RecordingStop(): void {
if (this.mediaRecorder) {
this.mediaRecorder.stop();
}
}
// 下载视频
public DownloadVideo(): void {
const blob = new Blob(this.recordedBlobsArrs, {
type: "video/webm; codecs=vp9",
});
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = "test.webm";
document.body.appendChild(a);
a.click();
// 销毁
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
}
// 开始加载视频
protected StartLoading(): void {
// 如果 pc有则关闭
if (this.pc) {
this.pc.close();
}
// webAPI 文档 https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/ontrack
this.pc = new RTCPeerConnection(undefined); // 创建rtc 连接
// 创建一个任务
this.pc.ontrack = <T extends IStreams>(event: T) => {
this.elVideo.srcObject = event.streams[0];
};
this.pc.addTransceiver("audio", { direction: "recvonly" });
this.pc.addTransceiver("video", { direction: "recvonly" });
this.pc
.createOffer()
.then((offer) => {
if (this.pc) {
return this.pc.setLocalDescription(offer).then(() => {
return offer;
});
}
})
.then((offer) => {
return new Promise((resolve, reject) => {
const port: number = Number(this.urlParams.port) || 1985;
// https://github.com/rtcdn/rtcdn-draft
let api: string = this.urlParams.user_query.play || "/rtc/v1/play/";
if (api.lastIndexOf("/") !== api.length - 1) {
api += "/";
}
let url: string =
"http://" + this.urlParams.server + ":" + port + api;
for (const key in this.urlParams.user_query) {
if (key !== "api" && key !== "play") {
url += "&" + key + "=" + this.urlParams.user_query[key];
}
}
// 请求参数
const parm: IRtcdnDraft = {
api: url,
streamurl: this.urlParams.url,
sdp: offer?.sdp,
clientip: null,
};
setTimeout(() => {
this.HttpPost(url, JSON.stringify(parm))
.then((res) => {
resolve(res.sdp);
})
.catch((err) => {
reject(err);
});
}, 4000);
});
})
.then((answer: any) => {
if (this.pc) {
// 替换 视频流服务器地址
const newServerIP: string =
this.opstion.replaceServerIP !== ""
? answer.replaceAll(
`${this.opstion.replaceServerIP}`,
this.opstion.replaceServerIPValue
)
: answer;
return this.pc.setRemoteDescription(
new RTCSessionDescription({ type: "answer", sdp: newServerIP })
);
}
})
.catch((err: any) => {
throw err;
});
}
// url 参数截取 &
protected ParmUrl(url: string): IParmUrl | null {
const query: string = url.split("?")[1]; // 截取 ? 后面的数据
if (query) {
const vars: string[] = query.split("&");
const obj: IParmUrl = {};
vars.forEach((item) => {
const pair = item.split("=");
obj[`${pair[0]}`] = pair[1];
});
return obj;
} else {
return null;
}
}
// url 参数截取
protected ParseUrl(): IUrlParams {
const ret: IUrlParams = {
app: "",
port: "",
schema: "",
server: "",
stream: "",
url: this.url,
user_query: {},
vhost: "",
};
const urlNoSchema: string = this.url.slice(9, this.url.length - 1);
const urlParmArr: string[] = urlNoSchema.split("/");
const setProt = urlParmArr[0].split(":");
ret.app = urlParmArr[1];
ret.port = setProt.length >= 2 ? setProt[1] : "1985";
ret.schema = this.url.substr(0, this.url.indexOf("://"));
ret.server = setProt[0];
ret.stream = urlParmArr[urlParmArr.length - 1];
ret.user_query = this.ParmUrl(ret.stream) ? this.ParmUrl(ret.stream) : {};
ret.vhost = ret.user_query.vhost;
return ret;
}
/**
* post 请求接口
*/
protected HttpPost(url: string, parm: string): Promise<AjaxResponse> {
return new Promise((resolve) => {
let xhr: any = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300) {
const respone = JSON.parse(xhr.responseText);
xhr = null;
resolve(respone);
}
};
xhr.open("POST", url, true);
xhr.timeout = 5000; // 5 seconds for timeout
xhr.responseType = "text";
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(parm);
});
}
}
webRTC 封装 SDK (由于年代久远,稍后补上完整代码)