You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何无需频繁请求服务器获取Icecast流元数据?

Icecast元数据推送优化方案

你当前用15秒轮询的方式确实会产生冗余请求,针对Icecast场景,有几种无需客户端主动轮询的服务器推送方案:

1. 直接解析音频流中的ICY元数据(最推荐)

Icecast基于ICY协议传输音频,协议本身会把曲目元数据嵌入到音频流中,不需要额外请求。自定义播放器可以直接读取这些元数据,完全避免轮询请求。

实现思路:

  • 音频请求时添加Icy-MetaData: 1请求头,告诉Icecast要发送元数据
  • 解析响应头中的icy-metaint值,这个值是音频块的大小,每播放完这么多字节,就会跟着一段元数据
  • 按格式解析元数据块,提取曲目信息

代码示例:

const audio = new Audio();
audio.crossOrigin = "anonymous";
let metaInterval = 0;
let bytesRead = 0;
let metaBuffer = [];

// 初始化音频请求
function setupAudioStream() {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', 'https://listen.abengnews.com/your-stream-url', true);
  xhr.setRequestHeader('Icy-MetaData', '1');
  xhr.responseType = 'arraybuffer';

  xhr.onload = function() {
    // 获取元数据间隔
    metaInterval = parseInt(xhr.getResponseHeader('icy-metaint'), 10);
    if (!metaInterval) return;

    // 创建MediaSource来处理流
    const mediaSource = new MediaSource();
    audio.src = URL.createObjectURL(mediaSource);

    mediaSource.addEventListener('sourceopen', function() {
      const sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');
      processStreamData(xhr.response, sourceBuffer);
    });
  };

  xhr.send();
}

// 处理音频流和元数据
function processStreamData(data, sourceBuffer) {
  const view = new DataView(data);
  while (bytesRead < view.byteLength) {
    if (bytesRead % metaInterval === 0) {
      // 读取元数据长度(实际长度是该值*16)
      const metaLength = view.getUint8(bytesRead) * 16;
      bytesRead += 1;

      if (metaLength > 0) {
        // 提取元数据
        const metaArray = new Uint8Array(view.buffer, bytesRead, metaLength);
        const metaString = new TextDecoder('utf-8').decode(metaArray).trim();
        bytesRead += metaLength;

        // 解析元数据(格式类似StreamTitle='Artist - Title';)
        const titleMatch = metaString.match(/StreamTitle='([^']+)';/);
        if (titleMatch) {
          updateTrackInfo(titleMatch[1]);
        }
      } else {
        bytesRead += metaLength;
      }
    }

    // 计算要添加的音频数据长度
    const chunkSize = Math.min(metaInterval - (bytesRead % metaInterval), view.byteLength - bytesRead);
    const audioChunk = view.buffer.slice(bytesRead, bytesRead + chunkSize);
    sourceBuffer.appendBuffer(audioChunk);
    bytesRead += chunkSize;
  }
}

// 更新页面曲目信息
function updateTrackInfo(title) {
  // 这里可以结合原服务器信息,或从元数据中提取更多内容
  document.getElementById("scroll-text").innerHTML = `Abeng News, News. Now playing: ${title}. Your server description`;
}

// 启动播放器
setupAudioStream();

2. 使用Server-Sent Events (SSE)

搭建一个简单的中间服务,低频监听Icecast的元数据变更(比如1分钟一次请求status-json.xsl),当元数据变化时,通过SSE推送给所有连接的客户端。

前端代码示例:

const eventSource = new EventSource('/icecast-meta-updates');
eventSource.onmessage = function(event) {
  const meta = JSON.parse(event.data);
  updateTrackInfo(meta.icestats.source.title);
};

function updateTrackInfo(title) {
  document.getElementById("scroll-text").innerHTML = `${meta.icestats.source.server_name}, ${meta.icestats.source.genre}. Now playing: ${title}. ${meta.icestats.source.server_description}`;
}

后端需自行实现,比如用Node.js定期拉取Icecast元数据,对比变化后通过SSE推送

3. WebSocket推送

和SSE类似,但支持双向通信,适合需要更多交互的场景。后端服务监听Icecast元数据变化,通过WebSocket推送给客户端。

前端代码示例:

const socket = new WebSocket('ws://your-server/icecast-meta');
socket.onmessage = function(event) {
  const meta = JSON.parse(event.data);
  updateTrackInfo(meta.icestats.source.title);
};

function updateTrackInfo(title) {
  document.getElementById("scroll-text").innerHTML = `${meta.icestats.source.server_name}, ${meta.icestats.source.genre}. Now playing: ${title}. ${meta.icestats.source.server_description}`;
}

原轮询代码参考

JavaScript

//Get the stream meta from xml
function ajax_get(url, callback) {
  'use strict';
  setInterval( function() {

  var xmlhttp = new XMLHttpRequest();
    xmlhttp.onreadystatechange = function() {
        if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
            //console.log('responseText:' + xmlhttp.responseText);
            try {
                var obj = JSON.parse(xmlhttp.responseText);
            } catch(err) {
                console.log(err.message + " in " + xmlhttp.responseText);
                return;
            }
            callback(obj);
        }
    };

    xmlhttp.open("GET", url, true);
    xmlhttp.send();
}, 15000);
}

ajax_get('https://listen.abengnews.com/status-json.xsl', function(obj) {
    document.getElementById("scroll-text").innerHTML = obj.icestats.source.server_name + ", " + obj.icestats.source.genre + ". Now playing: " + obj.icestats.source.title + ". " + obj.icestats.source.server_description;
    });

CSS

#scroll-container {
  /*position: absolute;*/
  background-color: #005500;
  border: 1px solid black;
  border-radius: 5px;
  overflow: hidden;
  width: 250px;
  margin: auto;
  font-family: arial,san-serif;
  font-size:10px;
  font-weight: bold;
  color: #fff; /* 修正原代码错误:font-color改为color */
}

#scroll-text {
  display: block;
  height:12px;
  white-space: nowrap;
  /* animation properties */
  -moz-transform: translateX(100%);
  -webkit-transform: translateX(100%);
  transform: translateX(100%);

  -moz-animation: my-animation 15s linear infinite;
  -webkit-animation: my-animation 15s linear infinite;
  animation: my-animation 15s linear infinite;
}
/* for Firefox */
@-moz-keyframes my-animation {
  from { -moz-transform: translateX(100%); }
  to { -moz-transform: translateX(-100%); }
}

/* for Chrome */
@-webkit-keyframes my-animation {
  from { -webkit-transform: translateX(100%); }
  to { -webkit-transform: translateX(-100%); }
}

@keyframes my-animation {
  from {
    -moz-transform: translateX(100%);
    -webkit-transform: translateX(100%);
    transform: translateX(100%);
  }
  to {
    -moz-transform: translateX(-100%);
    -webkit-transform: translateX(-100%);
    transform: translateX(-100%);
  }
}

HTML

<div id="scroll-container">
  <span class="scroll-text" id="scroll-text"></span>
</div>

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 00:44:57