yirtc-sdk-web
Version:
基于WebRTC的实时音视频SDK
413 lines (412 loc) • 13.2 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
/**
* YiRTC SDK 流类
*/
import { EventEmitter } from './utils/eventEmitter';
import { Logger } from './utils/logger';
import { VideoProfileType, MediaTypeEnum } from './types';
/**
* 流类,负责管理音视频流
*/
export class Stream extends EventEmitter {
/**
* 创建流实例
* @param config 流配置
*/
constructor(config) {
super();
this.stream = null;
this.videoTrack = null;
this.audioTrack = null;
this.videoElement = null;
this.userId = '';
this.isPlaying = false;
this.isLocal = true;
this.mediaType = MediaTypeEnum.VIDEO; // 默认为视频流
this.config = config;
this.logger = new Logger('Stream');
this.streamId = 'stream_' + Math.floor(Math.random() * 10000);
this.videoProfile = config.videoProfile || VideoProfileType.STANDARD;
// 根据配置设置媒体类型
if (config.screen) {
this.mediaType = MediaTypeEnum.SCREEN;
}
else if (config.video && config.audio) {
this.mediaType = MediaTypeEnum.VIDEO; // 音视频流默认标记为视频
}
else if (config.audio) {
this.mediaType = MediaTypeEnum.AUDIO;
}
else {
this.mediaType = MediaTypeEnum.VIDEO; // 默认为视频
}
this.logger.info('流已创建', { streamId: this.streamId, config, mediaType: this.mediaType });
}
/**
* 初始化流
* @returns Promise<void>
*/
init() {
return __awaiter(this, void 0, void 0, function* () {
try {
const constraints = {};
if (this.config.audio) {
constraints.audio = this.config.microphoneId ? { deviceId: { exact: this.config.microphoneId } } : true;
}
if (this.config.video) {
// 根据视频配置文件设置分辨率
let width = 640;
let height = 480;
switch (this.videoProfile) {
case VideoProfileType.LOW:
width = 160;
height = 120;
break;
case VideoProfileType.STANDARD:
width = 640;
height = 480;
break;
case VideoProfileType.HIGH:
width = 1280;
height = 720;
break;
case VideoProfileType.SUPER:
width = 1920;
height = 1080;
break;
}
constraints.video = {
deviceId: this.config.cameraId ? { exact: this.config.cameraId } : undefined,
width: { ideal: width },
height: { ideal: height }
};
}
else if (this.config.screen) {
// 屏幕共享
try {
// @ts-ignore
this.stream = yield navigator.mediaDevices.getDisplayMedia({
video: true,
audio: this.config.audio || false
});
}
catch (error) {
this.logger.error('获取屏幕共享失败', error);
throw error;
}
}
if (!this.stream && (this.config.audio || this.config.video)) {
this.stream = yield navigator.mediaDevices.getUserMedia(constraints);
}
if (this.stream) {
this.videoTrack = this.stream.getVideoTracks()[0] || null;
this.audioTrack = this.stream.getAudioTracks()[0] || null;
this.logger.info('流初始化成功', {
hasVideo: !!this.videoTrack,
hasAudio: !!this.audioTrack
});
this.emit('init-success');
}
else {
throw new Error('没有请求音频或视频');
}
}
catch (error) {
this.logger.error('流初始化失败', error);
this.emit('init-fail', { error });
throw error;
}
});
}
/**
* 播放流
* @param elementId 视频元素ID
* @returns void
*/
play(elementId) {
if (!this.stream) {
this.logger.error('流未初始化,无法播放');
return;
}
try {
const element = document.getElementById(elementId);
if (!element) {
this.logger.error(`找不到ID为 ${elementId} 的元素`);
return;
}
if (!(element instanceof HTMLVideoElement)) {
this.logger.error(`ID为 ${elementId} 的元素不是视频元素`);
return;
}
this.videoElement = element;
this.videoElement.srcObject = this.stream;
this.videoElement.onloadedmetadata = () => {
if (this.videoElement) {
this.videoElement.play()
.then(() => {
this.isPlaying = true;
this.emit('stream-played', { elementId });
this.logger.info('流开始播放', { elementId });
})
.catch(error => {
this.logger.error('播放流失败', error);
this.emit('play-failed', { error });
});
}
};
}
catch (error) {
this.logger.error('播放流失败', error);
throw error;
}
}
/**
* 停止播放
*/
stop() {
if (this.videoElement) {
this.videoElement.srcObject = null;
this.videoElement = null;
this.isPlaying = false;
this.emit('stream-stopped');
this.logger.info('流停止播放');
}
}
/**
* 销毁流
*/
destroy() {
this.stop();
if (this.stream) {
this.stream.getTracks().forEach(track => {
track.stop();
});
this.stream = null;
this.videoTrack = null;
this.audioTrack = null;
}
this.removeAllListeners();
this.logger.info('流已销毁', { streamId: this.streamId });
}
/**
* 静音音频
*/
muteAudio() {
if (this.audioTrack) {
this.audioTrack.enabled = false;
this.emit('audio-muted');
this.logger.info('音频已静音');
}
}
/**
* 取消静音音频
*/
unmuteAudio() {
if (this.audioTrack) {
this.audioTrack.enabled = true;
this.emit('audio-unmuted');
this.logger.info('音频已取消静音');
}
}
/**
* 禁用视频
*/
muteVideo() {
if (this.videoTrack) {
this.videoTrack.enabled = false;
this.emit('video-muted');
this.logger.info('视频已禁用');
}
}
/**
* 启用视频
*/
unmuteVideo() {
if (this.videoTrack) {
this.videoTrack.enabled = true;
this.emit('video-unmuted');
this.logger.info('视频已启用');
}
}
/**
* 设置音频配置
* @param profile 音频配置
*/
setAudioProfile(profile) {
this.logger.info('设置音频配置', { profile });
// 在实际应用中,这里需要设置音频的比特率等参数
}
/**
* 设置视频配置
* @param profile 视频配置
*/
setVideoProfile(profile) {
this.videoProfile = profile;
this.logger.info('设置视频配置', { profile });
// 在实际应用中,这里需要重新配置视频约束并重新获取媒体流
}
/**
* 设置音频音量
* @param volume 音量值 (0-100)
*/
setAudioVolume(volume) {
if (volume < 0 || volume > 100) {
this.logger.warn('音量值必须在0-100之间');
return;
}
if (this.videoElement) {
this.videoElement.volume = volume / 100;
this.logger.info('设置音频音量', { volume });
}
}
/**
* 获取音频电平
* @returns number 音频电平 (0-100)
*/
getAudioLevel() {
// 在实际应用中,这里应该使用Web Audio API分析音频电平
return this.audioTrack && this.audioTrack.enabled ? 50 : 0;
}
/**
* 是否有音频
* @returns boolean
*/
hasAudio() {
return !!this.audioTrack;
}
/**
* 是否有视频
* @returns boolean
*/
hasVideo() {
return !!this.videoTrack;
}
/**
* 是否正在播放
* @returns boolean
*/
isPlayingState() {
return this.isPlaying;
}
/**
* 是否可以播放
* @returns boolean
*/
canPlay() {
return !!this.stream;
}
/**
* 获取流ID
* @returns string
*/
getId() {
return this.streamId;
}
/**
* 获取用户ID
* @returns string | number
*/
getUserId() {
return this.userId;
}
/**
* 设置用户ID
* @param uid 用户ID
*/
setUserId(uid) {
this.userId = uid;
}
/**
* 设置是否为本地流
* @param isLocal 是否为本地流
*/
setIsLocal(isLocal) {
this.isLocal = isLocal;
}
/**
* 是否为本地流
* @returns boolean
*/
isLocalStream() {
return this.isLocal;
}
/**
* 获取MediaStream对象
* @returns MediaStream | null
*/
getMediaStream() {
return this.stream;
}
/**
* 截取视频快照
* @returns Promise<string> 图片的base64编码
*/
takeSnapshot() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.videoElement || !this.videoTrack) {
throw new Error('没有视频可截图');
}
try {
const canvas = document.createElement('canvas');
canvas.width = this.videoElement.videoWidth;
canvas.height = this.videoElement.videoHeight;
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('无法创建canvas上下文');
}
ctx.drawImage(this.videoElement, 0, 0, canvas.width, canvas.height);
const dataUrl = canvas.toDataURL('image/png');
this.logger.info('截图成功');
return dataUrl;
}
catch (error) {
this.logger.error('截图失败', error);
throw error;
}
});
}
/**
* 获取媒体类型
* @returns MediaType
*/
getMediaType() {
return this.mediaType;
}
/**
* 设置媒体类型
* @param mediaType 媒体类型
*/
setMediaType(mediaType) {
this.mediaType = mediaType;
this.logger.info('设置媒体类型', { mediaType });
}
/**
* 是否为屏幕共享流
* @returns boolean
*/
isScreenStream() {
return this.mediaType === MediaTypeEnum.SCREEN;
}
/**
* 是否为音频流
* @returns boolean
*/
isAudioStream() {
return this.mediaType === MediaTypeEnum.AUDIO;
}
/**
* 是否为视频流
* @returns boolean
*/
isVideoStream() {
return this.mediaType === MediaTypeEnum.VIDEO;
}
}