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

借助内置于所有现代网络浏览器的技术,您只需一个URL即可向其他人直播音频。本文将提供一步一步的指南来展示如何实现这一点。我们将使用 WebAudio 和 LiveKit 的 WebRTC 堆栈来构建一个实时应用程序,让您可以与朋友一起听音乐。查看完整的代码点击此处

还记得turntable.fm吗?它是一个社交DJ平台,用户可以加入虚拟房间一起听音乐。它在2011年推出时迅速风靡——在Twitter的TGIF活动中,我们把它放在一个大投影屏幕上,员工们轮流担任DJ。

Turntable.fm 截图

Turntable使用轮询请求进行同步,并使用Flash或隐藏的YouTube播放器来流式传输音频。如果我们今天来构建它的核心功能——实时一起听音乐——会怎么做呢?

我们将利用一些现代浏览器技术来实现这一目标

  • WebRTC是一套多功能的网络API,可用于构建各种实时应用程序。它也是在浏览器中获取UDP套接字的唯一标准化方式,这在低延迟流媒体传输时非常有用。
  • WebAudio是一个高级JavaScript API,用于在Web应用程序中处理和合成音频。

通常WebRTC用于实时与他人共享网络摄像头和/或麦克风流,但我们可以使用WebAudio从任何来源(例如URL)发布音频流。

在深入代码之前,请注意这里介绍的技术可以用于任何WebRTC堆栈,但我们使用LiveKit来大大简化操作。要实现同样的效果,请按照此迷你教程设置项目。

使用 MediaElementAudioSourceNode 和 MediaStreamAudioDestinationNode

WebAudio基于节点的概念工作,这些节点可以是音频数据的来源、修饰符或目的地。这些节点相互连接形成一个音频处理图。音频数据作为输入进入节点,流经图形,被沿途的每个节点处理,最终产生输出音频。

您可以使用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),您应该会从两个实例中听到播放的声音。我们在之前发布的空间音频演示中使用了完全相同的技术

0:00
/
来自空间音频示例应用的通过WebRTC播放音乐的视频

这是完整的代码,包括一些样板代码和为清晰起见而删减的部分

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" />
}
@/components/AudioURLPlayback.tsx
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>
  )
}
@/components/Room.tsx

建设者继续建设

您现在应该拥有在WebRTC会话中发布任意媒体所需的一切。如果您对此帖子有任何反馈,或者最终使用或受其启发构建了很酷的东西,请加入LiveKit Slack社区告诉我们。我们很高兴听到大家的声音!