Appearance
视频/直播/聊天方案
概述
实时音视频通信和即时通讯是现代应用的重要功能,涉及 WebRTC、WebSocket、流媒体等技术。
WebRTC 实时通信
1. 获取媒体设备
javascript
async function getUserMedia(constraints = { video: true, audio: true }) {
try {
const stream = await navigator.mediaDevices.getUserMedia(constraints)
return stream
} catch (error) {
console.error('获取媒体设备失败:', error)
throw error
}
}
// 使用示例
const localStream = await getUserMedia({
video: { width: 1280, height: 720 },
audio: true
})
// 显示本地视频
const localVideo = document.querySelector('#local-video')
localVideo.srcObject = localStream2. 创建 PeerConnection
javascript
const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'turn:your-turn-server.com', username: 'user', credential: 'pass' }
]
}
const peerConnection = new RTCPeerConnection(configuration)
// 添加本地流
localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream)
})
// 接收远程流
peerConnection.ontrack = (event) => {
const remoteVideo = document.querySelector('#remote-video')
remoteVideo.srcObject = event.streams[0]
}3. 信令交换
javascript
// 创建 Offer
async function createOffer() {
const offer = await peerConnection.createOffer()
await peerConnection.setLocalDescription(offer)
// 通过信令服务器发送 offer
signalingServer.send({
type: 'offer',
offer: offer
})
}
// 创建 Answer
async function createAnswer(offer) {
await peerConnection.setRemoteDescription(offer)
const answer = await peerConnection.createAnswer()
await peerConnection.setLocalDescription(answer)
// 通过信令服务器发送 answer
signalingServer.send({
type: 'answer',
answer: answer
})
}
// 接收 Answer
async function handleAnswer(answer) {
await peerConnection.setRemoteDescription(answer)
}直播推流
1. 推流到服务器
javascript
class LiveStreamer {
constructor(pushUrl) {
this.pushUrl = pushUrl
this.mediaRecorder = null
}
async start(stream) {
const options = {
mimeType: 'video/webm;codecs=vp9',
videoBitsPerSecond: 2500000
}
this.mediaRecorder = new MediaRecorder(stream, options)
this.mediaRecorder.ondataavailable = async (event) => {
if (event.data.size > 0) {
await this.sendChunk(event.data)
}
}
this.mediaRecorder.start(1000) // 每秒发送一次
}
async sendChunk(chunk) {
const formData = new FormData()
formData.append('chunk', chunk)
await fetch(this.pushUrl, {
method: 'POST',
body: formData
})
}
stop() {
if (this.mediaRecorder) {
this.mediaRecorder.stop()
}
}
}2. 拉流播放
javascript
class LivePlayer {
constructor(videoElement) {
this.video = videoElement
this.sourceBuffer = null
this.mediaSource = new MediaSource()
this.video.src = URL.createObjectURL(this.mediaSource)
this.mediaSource.addEventListener('sourceopen', () => {
this.sourceBuffer = this.mediaSource.addSourceBuffer('video/webm; codecs=vp9')
})
}
async play(pullUrl) {
const response = await fetch(pullUrl)
const reader = response.body.getReader()
while (true) {
const { done, value } = await reader.read()
if (done) break
this.sourceBuffer.appendBuffer(value)
}
}
}即时通讯
1. WebSocket 连接
javascript
class ChatClient {
constructor(url) {
this.url = url
this.ws = null
this.reconnectAttempts = 0
this.maxReconnectAttempts = 5
}
connect() {
this.ws = new WebSocket(this.url)
this.ws.onopen = () => {
console.log('WebSocket 连接成功')
this.reconnectAttempts = 0
}
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data)
this.handleMessage(message)
}
this.ws.onclose = () => {
console.log('WebSocket 连接关闭')
this.reconnect()
}
this.ws.onerror = (error) => {
console.error('WebSocket 错误:', error)
}
}
reconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++
setTimeout(() => {
this.connect()
}, 1000 * this.reconnectAttempts)
}
}
send(message) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message))
}
}
handleMessage(message) {
// 处理不同类型的消息
switch (message.type) {
case 'text':
this.onTextMessage(message)
break
case 'image':
this.onImageMessage(message)
break
case 'file':
this.onFileMessage(message)
break
}
}
}2. 消息存储
javascript
class MessageStore {
constructor() {
this.dbName = 'ChatDB'
this.dbVersion = 1
this.db = null
}
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.dbVersion)
request.onerror = () => reject(request.error)
request.onsuccess = () => {
this.db = request.result
resolve()
}
request.onupgradeneeded = (event) => {
const db = event.target.result
if (!db.objectStoreNames.contains('messages')) {
const store = db.createObjectStore('messages', { keyPath: 'id' })
store.createIndex('conversationId', 'conversationId', { unique: false })
store.createIndex('timestamp', 'timestamp', { unique: false })
}
}
})
}
async addMessage(message) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['messages'], 'readwrite')
const store = transaction.objectStore('messages')
const request = store.add(message)
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
}
async getMessages(conversationId, limit = 50) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['messages'], 'readonly')
const store = transaction.objectStore('messages')
const index = store.index('conversationId')
const request = index.getAll(conversationId)
request.onsuccess = () => {
const messages = request.result
.sort((a, b) => b.timestamp - a.timestamp)
.slice(0, limit)
resolve(messages)
}
request.onerror = () => reject(request.error)
})
}
}最佳实践
1. 性能优化
- 使用硬件加速编解码
- 实现自适应码率
- 优化网络传输
2. 用户体验
- 提供网络质量指示
- 支持美颜滤镜
- 实现回声消除
3. 安全性
- 端到端加密
- 身份验证
- 防止盗链
常见问题
如何处理网络波动?
- 实现断线重连
- 使用 FEC 前向纠错
- 自适应码率
如何降低延迟?
- 使用 UDP 传输
- 优化编码参数
- 减少缓冲区大小