使用 React 和 WebAudio 通过 WebRTC 流式传输音乐

借助内置于所有现代网络浏览器的技术,您只需一个URL即可向其他人直播音频。本文将提供一步一步的指南来展示如何实现这一点。我们将使用 WebAudio 和 LiveKit 的 WebRTC 堆栈来构建一个实时应用程序,让您可以与朋友一起听音乐。查看完整的代码点击此处。
还记得turntable.fm吗?它是一个社交DJ平台,用户可以加入虚拟房间一起听音乐。它在2011年推出时迅速风靡——在Twitter的TGIF活动中,我们把它放在一个大投影屏幕上,员工们轮流担任DJ。

Turntable使用轮询请求进行同步,并使用Flash或隐藏的YouTube播放器来流式传输音频。如果我们今天来构建它的核心功能——实时一起听音乐——会怎么做呢?
我们将利用一些现代浏览器技术来实现这一目标
- WebRTC是一套多功能的网络API,可用于构建各种实时应用程序。它也是在浏览器中获取UDP套接字的唯一标准化方式,这在低延迟流媒体传输时非常有用。
- WebAudio是一个高级JavaScript API,用于在Web应用程序中处理和合成音频。
通常WebRTC用于实时与他人共享网络摄像头和/或麦克风流,但我们可以使用WebAudio从任何来源(例如URL)发布音频流。
在深入代码之前,请注意这里介绍的技术可以用于任何WebRTC堆栈,但我们使用LiveKit来大大简化操作。要实现同样的效果,请按照此迷你教程设置项目。
使用 MediaElementAudioSourceNode 和 MediaStreamAudioDestinationNode
WebAudio基于节点的概念工作,这些节点可以是音频数据的来源、修饰符或目的地。这些节点相互连接形成一个音频处理图。音频数据作为输入进入节点,流经图形,被沿途的每个节点处理,最终产生输出音频。
您可以使用WebAudio组合出一些相当复杂的音频处理工作流,但我们的用例很简单

我们将使用<audio>元素作为 MediaElementAudioSourceNode 输入,并将其转换为 MediaStreamTrack(通过 MediaStreamAudioDestinationNode)输出。然后,我们可以将生成的输出作为WebRTC音轨发布。让我们在 React 组件中实现它
export const AudioURLPlayback = ({url}: AudioURLPlaybackProps) => {
const audioElRef = useRef<HTMLAudioElement>()
const audioCtxRef = useRef(new AudioContext())
useEffect(() => {
// ...
let audioSrc = audioCtxRef.current.createMediaElementSource(audioElRef.current),
audioDest = audioCtxRef.current.createMediaStreamDestination()
audioSrc.connect(audioDest)
}, [])
return <audio ref={audioElRef}
preload="auto"
url={url}
muted="false"
loop="true"
autoplay="true" />
}通过WebRTC会话发布 MediaStreamTrack
通过我们简单的WebAudio节点图连接好后,我们可以从中获取 MediaStreamTrack 作为输出,并将其发布给连接到同一WebRTC会话的其他听众
import { Track } from "livekit-client"
import { useLocalParticipant } from "@livekit/components-react"
export const AudioURLPlayback = ({url}: AudioURLPlaybackProps) => {
// ...
const { localParticipant } = useLocalParticipant()
useEffect(() => {
// ...
let publishedTrack = audioDest.stream.getAudioTracks()[0]
localParticipant.publishTrack(publishedTrack, {
name: "audio_from_url",
source: Track.Source.Unknown
})
}, [])
// ...
}开始WebRTC会话
现在我们有了将URL中的音频拉取到WebRTC音轨并在实时会话中发布的代码,剩下的就是创建会话的一些样板代码了
import { LiveKitRoom } from "@livekit/components-react"
import { AudioURLPlayback } from "./your_component_path"
type AudioURLPlaybackProps { url: string | null }
export const Room = ({ token, wsUrl, audioUrl }: RoomProps) => {
return (
<LiveKitRoom token={token} serverUrl={wsUrl} connect={true}>
<AudioURLPlayback url={audioUrl} />
</LiveKitRoom>
)
}然后,砰!💥 实际上就是这么简单。如果您在几个浏览器标签中运行这段代码(注意:确保只有一个用户发布有效的音频URL),您应该会从两个实例中听到播放的声音。我们在之前发布的空间音频演示中使用了完全相同的技术
这是完整的代码,包括一些样板代码和为清晰起见而删减的部分
import { useEffect, useRef, useCallback } from "react";
import { Track } from "livekit-client";
import { useLocalParticipant } from "@livekit/components-react";
export const AudioURLPlayback = ({url}: AudioURLPlaybackProps) => {
const audioElContainerRef = useRef<HTMLDivElement>();
const audioElRef = useRef<HTMLAudioElement>();
const audioSourceNodeRef = useRef<MediaElementAudioSourceNode | null>(null);
const audioContextRef = useRef(new AudioContext());
const audioDestinationNodeRef = useRef<MediaStreamAudioDestinationNode | null>(null);
const publishedTrack = useRef<MediaStreamTrack | null>(null);
const { localParticipant } = useLocalParticipant();
const cleanup = useCallback(() => {
if(publishedTrack.current) {
localParticipant.unpublishTrack(publishedTrack.current);
publishedTrack.current = null;
}
if(audioSourceNodeRef.current) {
audioSourceNodeRef.current.disconnect();
audioSourceNodeRef.current = null;
}
if (audioDestinationNodeRef.current) {
audioDestinationNodeRef.current.disconnect();
audioDestinationNodeRef.current = null;
}
if (audioElRef.current) {
audioElRef.current.pause();
audioElRef.current.remove();
audioElRef.current = null;
}
}, []);
useEffect(() => {
if(!url) cleanup();
// Create audio source and store it
audioSourceNodeRef.current = audioContextRef.current.createMediaElementSource(audioElRef.current);
// Connect audio source node to a MediaStreamAudioDestinationNode
audioDestinationNodeRef.current = audioContextRef.current.createMediaStreamDestination();
audioSourceNodeRef.current.connect(audioDestinationNodeRef.current);
// Publish audio to LiveKit room
publishedTrack.current = audioDestinationNodeRef.current.stream.getAudioTracks()[0]
localParticipant.publishTrack(publishedTrack.current, {
name: "audio_from_url",
source: Track.Source.Unknown,
});
return cleanup();
}, [cleanup]);
return <audio ref={audioElRef}
preload="auto"
url={url}
muted="false"
loop="true"
autoplay="true" />
}import { LiveKitRoom } from "@livekit/components-react";
import { AudioURLPlayback } from "@/components/AudioURLPlayback";
type UrlPlaybackProps {
url: string | null
}
type RoomProps {
token: string
wsUrl: string
}
export const Room = ({token, wsUrl}: RoomProps) => {
// set this to whatever you like
const [audioUrl, setAudioUrl] = useState<string | null>(null);
return (
<LiveKitRoom token={token} serverUrl={wsUrl} connect={true}>
<AudioURLPlayback url={audioUrl} />
</LiveKitRoom>
)
}建设者继续建设
您现在应该拥有在WebRTC会话中发布任意媒体所需的一切。如果您对此帖子有任何反馈,或者最终使用或受其启发构建了很酷的东西,请加入LiveKit Slack社区告诉我们。我们很高兴听到大家的声音!