如何无需频繁请求服务器获取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
相关产品推荐
相关产品推荐

