You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

如何让所有用户获取顺序一致的随机歌曲?在线电台技术问询

解决方案:实现全局一致的无限随机播放列表

很棒的在线电台项目!要解决节目结束后所有用户听到完全一致随机播放列表的问题,核心是放弃客户端各自的随机逻辑,改用全局共享的确定性伪随机序列——让所有客户端基于同一个固定种子生成播放顺序,这样不管哪个用户进入,播放列表都会完全同步。下面是贴合你现有代码的具体实现方案:

一、核心逻辑拆解

要保证所有用户随机顺序一致,必须满足两个条件:

  1. 所有客户端使用同一个随机种子(由服务端统一提供)
  2. 使用可复现的伪随机数生成器(不能用原生Math.random(),因为它的种子是客户端本地随机的)

二、具体实现步骤

1. 服务端提供共享随机种子

我们可以用节目结束的Unix时间戳作为固定种子(不用额外存储,所有用户都能通过计算得到),也可以生成一个随机数存在缓存里。这里修改你的timestamp.php,让它在节目结束后返回种子:

<?php
// 从playlist.xml的showtime计算节目结束时间(假设节目时长1小时)
$show_start_time = 1526448420; // 可改为从playlist.xml读取,此处为硬编码示例
$show_end_time = $show_start_time + 3600;

if (time() >= $show_end_time) {
    // 节目结束,返回固定种子(用节目结束时间,确保所有用户拿到的种子一致)
    echo $show_end_time;
} else {
    // 节目进行中,返回当前时间戳
    echo time() + (int)$_REQUEST['time'];
}
?>

2. 客户端实现可复现的伪随机数生成器

原生Math.random()无法保证一致性,我们自己实现一个简单可靠的线性同余生成器(LCG),基于种子生成可复现的随机数:

// 带种子的伪随机数生成器(LCG算法,可复现)
function SeededRandom(seed) {
    this.seed = seed % 2147483647;
    if (this.seed <= 0) this.seed += 2147483646;
}

// 生成下一个随机整数
SeededRandom.prototype.next = function() {
    return this.seed = this.seed * 16807 % 2147483647;
};

// 返回0-1之间的浮点数(兼容Math.random()的使用方式)
SeededRandom.prototype.random = function() {
    return (this.next() - 1) / 2147483646;
};

3. 加载全数据库并生成同步随机列表

节目结束后,加载所有流派的XML数据库,用共享种子打乱歌曲顺序,生成无限循环的播放列表:

// 全局变量存储全库歌曲、随机生成器、当前播放索引
var allTracks = [];
var randomGenerator;
var currentRandomIndex = 0;

// 加载所有流派数据库
function loadAllGenreDatabases() {
    var genreFiles = ["genre1.xml", "genre2.xml", "genre3.xml"]; // 替换成你的实际文件名
    var loadedCount = 0;

    genreFiles.forEach(file => {
        load_prs(XML => {
            // 解析XML中的所有歌曲,存入allTracks
            Array.from(XML.querySelectorAll('track')).forEach(trackNode => {
                const band = get_val(trackNode, "band");
                const album = get_val(trackNode, "album");
                const title = get_val(trackNode, "title");
                const genre = get_val(trackNode, "genre");
                const length = get_val(trackNode, "length");
                const tid = trackNode.getAttribute("id");
                const tsrc = `songs/${tid}.mp3`;
                
                allTracks.push(new tobj(band, album, title, genre, length, tsrc));
            });

            loadedCount++;
            if (loadedCount === genreFiles.length) {
                // 所有数据库加载完成,初始化随机播放
                initRandomPlaylist();
            }
        }, file);
    });
}

// 初始化随机播放逻辑
function initRandomPlaylist() {
    // 从服务端获取共享种子
    load_prs(resp => {
        const seed = Number(resp);
        randomGenerator = new SeededRandom(seed);
        // 用种子打乱歌曲列表(确定性洗牌)
        shuffleTracks();
        // 开始播放第一首随机歌曲
        playRandomTrack();
    }, "timestamp.php?time=0");
}

// 基于种子的Fisher-Yates洗牌(保证所有用户打乱顺序一致)
function shuffleTracks() {
    let currentIndex = allTracks.length;
    let randomIndex;

    while (currentIndex !== 0) {
        // 用自定义随机生成器获取索引
        randomIndex = Math.floor(randomGenerator.random() * currentIndex);
        currentIndex--;
        // 交换元素
        [allTracks[currentIndex], allTracks[randomIndex]] = [allTracks[randomIndex], allTracks[currentIndex]];
    }
}

// 播放随机歌曲,循环往复
function playRandomTrack() {
    // 列表播放完就重新打乱(基于当前生成器状态,保证每次打乱顺序不同但可复现)
    if (currentRandomIndex >= allTracks.length) {
        shuffleTracks();
        currentRandomIndex = 0;
    }

    const track = allTracks[currentRandomIndex];
    track_obj = track;
    track_length = track.length;

    // 复用你现有的播放器更新逻辑
    track_src.src = track.tsrc;
    player_title.textContent = `${track.band} - ${track.title}`;
    player_album.textContent = `Album: ${track.album}`;
    
    track_audio.onended = () => {
        currentRandomIndex++;
        playRandomTrack();
    };

    track_audio.load();
    track_audio.play();
}

4. 修改现有播放流程,触发随机模式

在你的next_track()函数中,判断节目是否已经结束,结束后切换到随机播放:

function next_track() {
    current_play++;
    // 这里需要保留节目playlist的XML引用,或者重新加载获取showtime
    const playlistXML = /* 你的节目playlist XML对象 */;
    const showStartTime = Number(playlistXML.getAttribute("showtime"));
    const showEndTime = showStartTime + 3600; // 1小时节目时长

    // 判断是否节目已结束,或者预定义列表已播完
    if (current_play >= playlistXML.children.length || timestamp >= showEndTime) {
        // 切换到随机播放模式
        loadAllGenreDatabases();
        return;
    }

    // 继续预定义节目播放
    track_data_complete(playlistXML);
}

三、为什么这个方案比SSE更简便?

  • 无长连接开销:SSE需要服务端维持与每个客户端的长连接,而这个方案只需要一次种子请求,客户端本地生成序列,大幅降低服务端压力。
  • 兼容现有代码:完全复用你现有的播放器逻辑,只需要新增随机生成和洗牌部分。
  • 鲁棒性强:即使客户端断开重连,只要重新获取种子,就能快速同步到当前全局播放进度(可以基于时间计算当前应该播放的歌曲和位置)。

四、额外优化建议

  • 缓存全数据库:第一次加载后把所有歌曲数据存入localStorage,下次打开无需重新加载XML,提升加载速度。
  • 精准同步进度:用户中途进入时,计算从节目结束到当前的总秒数,累加随机歌曲的时长,定位到当前应该播放的歌曲和播放位置,完全同步全局进度。

内容的提问来源于stack exchange,提问作者Xavier

火山引擎 最新活动