React全Hook项目实战在线聊天室历程(三):加个音乐直播?

wuchangjian2021-11-16 17:11:57编程学习

前情提要:

React全Hook项目实战在线聊天室历程(一):基本功能
React全Hook项目实战在线聊天室历程(二):引用与话题功能

正文

聊天应该有什么?背景音乐,茶与酒,零食,后两个我是没法实现了,但是我们可以给我们的在线聊天室加一个背景音乐的功能。

初步设想就是写好一堆音乐的外链在前端,然后让它自动播放,但是这样没有背景音乐的感觉,因为大家听到的歌都不一样呀。

那么能不能做一个“直播”呢,让大家听到的是同一首歌,同一个进度?

通过查阅资料,我大概找到两个方法,一个是用RTMP协议+ffmpeg推流,前端再接收。第二个方法是让后端把音乐文件切成一段一段加好头部信息再通过WebSocket传给前端,前端拿到后直接播放。这里加头部信息的方法参考了这位大佬的博客blob语音流 前端播放。

两个方法都有一个弊端就是,很烧流量而且对服务器的负担很大,耗钱。而且第二个方法还有一个问题我没想明白就是,该间隔多久发送一段数据,如果有知道的朋友希望再评论区里告诉我。

所以想想就找个投机取巧的方法:在服务端列好歌单,歌单包括歌曲时长以及歌曲的URL,然后服务端跑一个计时器,通过这个计时器来计算当前歌曲进度,然后到时间了就通过WebSocket广播歌曲信息和进度。前端一进来的时候会首先收到歌曲信息和进度,然后把URL放进<audio>,设置好currentTime再自动播放就好了。

服务端:

// ws.js
// 放了音乐的配置文件
/* ...... */
var radio_config = require("../upload/music/config.json");
var process_s = 0; // 进度(秒)
var song_index = 0; // 歌曲索引
/* ...... */
// 计算播放进度
function calcRadioProcess() {
    setInterval(() => {
        if (process_s > radio_config[song_index].duration) {
            song_index = song_index + 1 >= radio_config.length ? 0 : song_index + 1
            process_s = 0
            console.log("切歌", radio_config[song_index])
            bc(clientList, JSON.stringify({
                type: 'song',
                song: radio_config[song_index],
                current: 0
            }))
        } else {
            process_s += 1
        }
    }, 1000)
}
calcRadioProcess()
router.get("/getRadioProcess", (req, res) => {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.end(JSON.stringify({
        "success": true,
        "data": {
            type: 'song',
            song: radio_config[song_index],
            current: process_s
        }
    }))
})
/* ...... */
// 
router.ws("/", function (ws, req) {
    ws.clientId = req.query.id
    clientList.push(ws)
    console.log("新IP" + req.query.id);
    console.log("当前在线人数" + clientList.length);
    // websocket一连进来的时候就告诉现在正在播放的音乐与进度
    ws.send(JSON.stringify({
        type: 'song',
        song: radio_config[song_index],
        current: process_s
    }))
/* ...... */

前端
写个Radio类

import { useReducer, useRef, useEffect, useImperativeHandle, useState } from "react";
import { forwardRef } from "react";

function musicListReducer(state, action) {
    switch (action.type) {
        case 'add':
            return [...state, action.data]
        case 'shift':
            return state.slice(1)
        case 'init':
            return [action.data]
        default:
            throw new Error("错误的action.type");
    }
}
var MyRadio = forwardRef((props, ref) => {
    // 音乐列表
    const [musicList, setMusicList] = useReducer(musicListReducer, [])
    function setMusic(data) {
        if (firstClick === false) {
            setMusicList({ type: 'init', data: data })
        } else {
            setMusicList({ type: 'add', data: data })
        }
    }
    // 播完自动切歌
    useEffect(() => {
        audioRef.current.onended = () => {
            setMusicList({ type: 'shift' })
        }
    }, [])

    // 由于谷歌浏览器新协议限制不能自动播放
    // 默认进来的时候是静音
    // 点击后开始播放
    const [playing, setPlaying] = useState(false);
    const [firstClick, setFirstClick] = useState(false);
    const audioRef = useRef(null);
    function setCurrentTime(sec) {
        audioRef.current.currentTime = sec;
    }
    function switchPlaying() {
        if (firstClick === false) {
            setFirstClick(true)
            // 如果是第一次开播,需要先获取到第一首歌的进度
            fetch("http://localhost:8080/ws/getRadioProcess").then((response) => {
                return response.json()
            }).then(json => {
                const data = json.data
                setCurrentTime(data.current)
                audioRef.current.play();
            })
        }
        setPlaying(!playing);
    }

    useImperativeHandle(ref, () => ({
        setMusic,
        setCurrentTime
    }))
    // 用emoji吧,懒得弄SVG
    return (
        <div onClick={switchPlaying}>
            <audio style={{ display: 'none' }} src={musicList[0]?.url} ref={audioRef} autoPlay muted={!playing}></audio>
            {playing ? <span>🔈</span> : <span>🔕</span>}
            <span>正在播放:{musicList[0]?.name}</span>
        </div>
    )
})

export default MyRadio

小插曲

如果你的浏览器是谷歌浏览器的话,直接设置audio为autoplay,或者直接调用audio.play(),会报错DOMException: play() failed because the user didn't interact with the document first.是因为较新版本的谷歌浏览器不允许自动播放。所以一开始进来的时候是静音的,只有用户点击之后才会开始播放音乐,又因为用户点击websocket建立的时间又有一段时间的差距,所以第一次点击的时候还要再同步一次进度,之后就可以自动播放了。

相关文章

03-树2 List Leaves (25 分)

# include<stdio.h> # include<queue&...

PDF文件限制密码如何取消

PDF文件带有限制密码,每次想要编辑都要将限制密码取消才能编辑࿰...

DevOps自动化之Jenkins

一、DevOps介绍 DevOps 一词的来自于 Development 和 Oper...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。