使用PHP从音频流中提取轨道信息

21

是否可以使用PHP从音频流中获取曲目信息?我已经尝试过一些方法,最接近的函数是stream_get_transports,但是我的主机不支持通过fsockopen()进行http传输,所以我需要进一步调整来查看该函数返回什么内容。

目前,我正在尝试从AOL流中提取艺术家和曲目元数据。


http://scfire-dtc-aa01.stream.aol.com:80/stream/1003。这个URL是特定的,但我正在从Digitally Imported播放列表(pls文件)中提取数据。 - Levi Hackwith
1
我想我在phpclasses.org上看到了一些用于处理mp3标签元信息等音频处理类。去看看吧,那里有很多好东西。如果不知道你需要什么数据,很难推荐具体的内容。 - d-_-b
5个回答

55
这是一个SHOUTcast流,是可以实现的。这与ID3标签完全无关。我以前写过一个脚本来实现这个功能,但现在找不到了。就在上周,我帮助了另一个人,他有一个相当完整的脚本来实现同样的功能,但我不能随便发布源代码,因为那不是我的。不过,如果你给我发电子邮件brad@musatcha.com,我可以把你联系给他。
无论如何,下面是如何自己实现这个功能:
首先,你需要直接连接到服务器,不要使用HTTP。你可以使用cURL,但可能会比较麻烦。你可以使用fsockopen()函数(doc)来连接服务器。确保使用正确的端口。还要注意,许多网络主机会阻止许多端口,但通常可以使用端口80。幸运的是,所有AOL托管的SHOUTcast流都使用端口80。
现在,像客户端一样发送你的请求。

GET /whatever HTTP/1.0

但是,在发送<CrLf><CrLf>之前,请包含下一个标头!

Icy-MetaData:1

这告诉服务器您想要元数据。现在,发送您的一对<CrLf>

好的,服务器将以一系列标头作为响应,然后开始向您发送数据。在这些标头中,将包含一个icy-metaint:8192或类似的值。这个8192是元间隔。这很重要,也是您唯一需要的值。通常是8192,但并不总是,所以确保实际阅读此值!

基本上,这意味着您将获得8192字节的MP3数据,然后是一块元数据块,然后是8192字节的MP3数据,然后是一块元数据块。

读取8192字节的数据(确保不包括头部在内),丢弃它们,然后读取下一个字节。这个字节是元数据的第一个字节,表示元数据的长度。使用这个字节的值(使用ord()函数(doc)获取实际字节),并将其乘以16。结果就是要读取的元数据字节数。将这些字节数读入一个字符串变量中,以便进行处理。
接下来,修剪这个变量的值。为什么要修剪?因为字符串末尾填充了0x0(以使其能够完全适应16字节的倍数),而trim()函数(doc)会为我们处理这个问题。
你将得到类似于以下内容: StreamTitle='Awesome Trance Mix - DI.fm';StreamUrl='' 我会让你选择你喜欢的方法来解析这个。个人而言,我可能会使用;进行分割,但要注意包含;的标题。我不确定转义字符的方法是什么。稍微试验一下应该会帮助你。
完成后不要忘记与服务器断开连接!
有很多关于SHOUTcast元数据的参考资料。这是一个很好的参考链接:https://web.archive.org/web/20191121035806/http://www.smackfu.com/stuff/programming/shoutcast.html

你应该注意 ' 而不是 ; - ctn
您知道这个程序使用了哪种编码(UTF-8、ISO-8859等)以及标题或URL中是否需要对任何“'”字符进行转义吗? - Thomas
1
@Thomas 这取决于不同的情况。有些站点使用ISO-8859编码,而现在许多站点使用UTF-8编码。客户端支持也各不相同。转义字符也是如此。一些频道会使用反斜杠\来转义单引号,而另一些则会将其重复两次。不幸的是,这方面没有真正的标准。 - Brad

19

看这个:https://gist.github.com/fracasula/5781710

它是一个小的代码片段,包含了一个PHP函数,可以从流媒体URL中提取MP3元数据(StreamTitle)。

通常,流媒体服务器会在响应中放置一个icy-metaint标头,告诉我们元数据在流中发送的频率。该函数检查该响应标头,并且如果存在,则替换间隔参数。

否则,该函数通过递归调用流媒体URL,在保持您设置的时间间隔的同时尝试从偏移量参数开始提取元数据。

<?php

/**
 * Please be aware. This gist requires at least PHP 5.4 to run correctly.
 * Otherwise consider downgrading the $opts array code to the classic "array" syntax.
 */
function getMp3StreamTitle($streamingUrl, $interval, $offset = 0, $headers = true)
{
    $needle = 'StreamTitle=';
    $ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36';

    $opts = [
            'http' => [
            'method' => 'GET',
            'header' => 'Icy-MetaData: 1',
            'user_agent' => $ua
        ]
    ];

    if (($headers = get_headers($streamingUrl))) {
        foreach ($headers as $h) {
            if (strpos(strtolower($h), 'icy-metaint') !== false && ($interval = explode(':', $h)[1])) {
                break;
            }
        }
    }

    $context = stream_context_create($opts);

    if ($stream = fopen($streamingUrl, 'r', false, $context)) {
        $buffer = stream_get_contents($stream, $interval, $offset);
        fclose($stream);

        if (strpos($buffer, $needle) !== false) {
            $title = explode($needle, $buffer)[1];
            return substr($title, 1, strpos($title, ';') - 2);
        } else {
            return getMp3StreamTitle($streamingUrl, $interval, $offset + $interval, false);
        }
    } else {
        throw new Exception("Unable to open stream [{$streamingUrl}]");
    }
}

var_dump(getMp3StreamTitle('http://str30.creacast.com/r101_thema6', 19200));

我希望这能帮到你!


解析错误:C:\ wamp \ www \ stream \ index.php第8行意外的'[',请检查代码。 - AtanuCSE
3
你必须使用至少 PHP 5.4。否则,请尝试使用经典的 array 语法转换 $opts 数组。 - Francesco Casula
这个可以运行,但是获取结果需要太长时间。如何进行优化? - csr-nontol

3

非常感谢fra_casula提供的代码。这里是一个略微简化的版本,适用于PHP <= 5.3(原版针对5.4)。它还重复使用了相同的连接资源。

由于我的需求,我删除了异常处理,如果没有找到任何内容,则返回false。

    private function getMp3StreamTitle($steam_url)
    {
        $result = false;
        $icy_metaint = -1;
        $needle = 'StreamTitle=';
        $ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36';

        $opts = array(
            'http' => array(
                'method' => 'GET',
                'header' => 'Icy-MetaData: 1',
                'user_agent' => $ua
            )
        );

        $default = stream_context_set_default($opts);

        $stream = fopen($steam_url, 'r');

        if($stream && ($meta_data = stream_get_meta_data($stream)) && isset($meta_data['wrapper_data'])){
            foreach ($meta_data['wrapper_data'] as $header){
                if (strpos(strtolower($header), 'icy-metaint') !== false){
                    $tmp = explode(":", $header);
                    $icy_metaint = trim($tmp[1]);
                    break;
                }
            }
        }

        if($icy_metaint != -1)
        {
            $buffer = stream_get_contents($stream, 300, $icy_metaint);

            if(strpos($buffer, $needle) !== false)
            {
                $title = explode($needle, $buffer);
                $title = trim($title[1]);
                $result = substr($title, 1, strpos($title, ';') - 2);
            }
        }

        if($stream)
            fclose($stream);                

        return $result;
    }

2
这是使用 HttpClient 获取元数据的 C# 代码:
public async Task<string> GetMetaDataFromIceCastStream(string url)
    {
        m_httpClient.DefaultRequestHeaders.Add("Icy-MetaData", "1");
        var response = await m_httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
        m_httpClient.DefaultRequestHeaders.Remove("Icy-MetaData");
        if (response.IsSuccessStatusCode)
        {
            IEnumerable<string> headerValues;
            if (response.Headers.TryGetValues("icy-metaint", out headerValues))
            {
                string metaIntString = headerValues.First();
                if (!string.IsNullOrEmpty(metaIntString))
                {
                    int metadataInterval = int.Parse(metaIntString);
                    byte[] buffer = new byte[metadataInterval];
                    using (var stream = await response.Content.ReadAsStreamAsync())
                    {
                        int numBytesRead = 0;
                        int numBytesToRead = metadataInterval;
                        do
                        {
                            int n = stream.Read(buffer, numBytesRead, 10);
                            numBytesRead += n;
                            numBytesToRead -= n;
                        } while (numBytesToRead > 0);

                        int lengthOfMetaData = stream.ReadByte();
                        int metaBytesToRead = lengthOfMetaData * 16;
                        byte[] metadataBytes = new byte[metaBytesToRead];
                        var bytesRead = await stream.ReadAsync(metadataBytes, 0, metaBytesToRead);
                        var metaDataString = System.Text.Encoding.UTF8.GetString(metadataBytes);
                        return metaDataString;
                    }
                }
            }
        }

        return null;
    }

你的代码是否从URL中获取元数据,还是在流继续时也更新参数,例如艺术家等? - Or Assraf

0
更新: 这是一个更合适的解决方案来回答这个问题。原始帖子也在下面提供了信息。
在进行一些错误修正后,本帖中的脚本可以使用PHP工作并提取流标题: 从Shoutcast/Icecast流中提取艺术家和标题的PHP脚本
我不得不做出一些更改,因为最后的echo语句会抛出错误。我在函数后添加了两个print_r()语句,并在调用时加入了$argv[1],这样你就可以从命令行传递URL给它。
<?php
define('CRLF', "\r\n");

class streaminfo{
public $valid = false;
public $useragent = 'Winamp 2.81';

protected $headers = array();
protected $metadata = array();

public function __construct($location){
    $errno = $errstr = '';
    $t = parse_url($location);
    $sock = fsockopen($t['host'], $t['port'], $errno, $errstr, 5);
    $path = isset($t['path'])?$t['path']:'/';
    if ($sock){
        $request = 'GET '.$path.' HTTP/1.0' . CRLF . 
            'Host: ' . $t['host'] . CRLF . 
            'Connection: Close' . CRLF . 
            'User-Agent: ' . $this->useragent . CRLF . 
            'Accept: */*' . CRLF . 
            'icy-metadata: 1'.CRLF.
            'icy-prebuffer: 65536'.CRLF.
            (isset($t['user'])?'Authorization: Basic '.base64_encode($t['user'].':'.$t['pass']).CRLF:'').
            'X-TipOfTheDay: Winamp "Classic" rulez all of them.' . CRLF . CRLF;
        if (fwrite($sock, $request)){
            $theaders = $line = '';
            while (!feof($sock)){ 
                $line = fgets($sock, 4096); 
                if('' == trim($line)){
                    break;
                }
                $theaders .= $line;
            }
            $theaders = explode(CRLF, $theaders);
            foreach ($theaders as $header){
                $t = explode(':', $header); 
                if (isset($t[0]) && trim($t[0]) != ''){
                    $name = preg_replace('/[^a-z][^a-z0-9]*/i','', strtolower(trim($t[0])));
                    array_shift($t);
                    $value = trim(implode(':', $t));
                    if ($value != ''){
                        if (is_numeric($value)){
                            $this->headers[$name] = (int)$value;
                        }else{
                            $this->headers[$name] = $value;
                        }
                    }
                }
            }
            if (!isset($this->headers['icymetaint'])){
                $data = ''; $metainterval = 512;
                while(!feof($sock)){
                    $data .= fgetc($sock);
                    if (strlen($data) >= $metainterval) break;
                }
               $this->print_data($data);
                $matches = array();
                preg_match_all('/([\x00-\xff]{2})\x0\x0([a-z]+)=/i', $data, $matches, PREG_OFFSET_CAPTURE);
               preg_match_all('/([a-z]+)=([a-z0-9\(\)\[\]., ]+)/i', $data, $matches, PREG_SPLIT_NO_EMPTY);
               echo '<pre>';var_dump($matches);echo '</pre>';
                $title = $artist = '';
                foreach ($matches[0] as $nr => $values){
                  $offset = $values[1];
                  $length = ord($values[0]{0}) + 
                            (ord($values[0]{1}) * 256)+ 
                            (ord($values[0]{2}) * 256*256)+ 
                            (ord($values[0]{3}) * 256*256*256);
                  $info = substr($data, $offset + 4, $length);
                  $seperator = strpos($info, '=');
                  $this->metadata[substr($info, 0, $seperator)] = substr($info, $seperator + 1);
                    if (substr($info, 0, $seperator) == 'title') $title = substr($info, $seperator + 1);
                    if (substr($info, 0, $seperator) == 'artist') $artist = substr($info, $seperator + 1);
                }
                $this->metadata['streamtitle'] = $artist . ' - ' . $title;
            }else{
                $metainterval = $this->headers['icymetaint'];
                $intervals = 0;
                $metadata = '';
                while(1){
                    $data = '';
                    while(!feof($sock)){
                        $data .= fgetc($sock);
                        if (strlen($data) >= $metainterval) break;
                    }
                    //$this->print_data($data);
                    $len = join(unpack('c', fgetc($sock))) * 16;
                    if ($len > 0){
                        $metadata = str_replace("\0", '', fread($sock, $len));
                        break;
                    }else{
                        $intervals++;
                        if ($intervals > 100) break;
                    }
                }
                $metarr = explode(';', $metadata);
                foreach ($metarr as $meta){
                    $t = explode('=', $meta);
                    if (isset($t[0]) && trim($t[0]) != ''){
                        $name = preg_replace('/[^a-z][^a-z0-9]*/i','', strtolower(trim($t[0])));
                        array_shift($t);
                        $value = trim(implode('=', $t));
                        if (substr($value, 0, 1) == '"' || substr($value, 0, 1) == "'"){
                            $value = substr($value, 1);
                        }
                        if (substr($value, -1) == '"' || substr($value, -1) == "'"){
                            $value = substr($value, 0, -1);
                        }
                        if ($value != ''){
                            $this->metadata[$name] = $value;
                        }
                    }
                }
            }

            fclose($sock);
            $this->valid = true;
        }else echo 'unable to write.';
    }else echo 'no socket '.$errno.' - '.$errstr.'.';

print_r($theaders);
print_r($metadata);
}

public function print_data($data){
    $data = str_split($data);
    $c = 0;
    $string = '';
    echo "<pre>\n000000 ";
    foreach ($data as $char){
        $string .= addcslashes($char, "\n\r\0\t");
        $hex = dechex(join(unpack('C', $char)));
        if ($c % 4 == 0) echo ' ';
        if ($c % (4*4) == 0 && $c != 0){
          foreach (str_split($string) as $s){
            //echo " $string\n";
            if (ord($s) < 32 || ord($s) > 126){
              echo '\\'.ord($s);
            }else{
              echo $s;
            }
          }
          echo "\n";
          $string = '';
          echo str_pad($c, 6, '0', STR_PAD_LEFT).'  ';
        }
        if (strlen($hex) < 1) $hex = '00';
        if (strlen($hex) < 2) $hex = '0'.$hex;
          echo $hex.' ';
        $c++;
    }
    echo "  $string\n</pre>";
}

public function __get($name){
    if (isset($this->metadata[$name])){
        return $this->metadata[$name];
    }
    if (isset($this->headers[$name])){
        return $this->headers[$name];
    }
    return null;
 }
}

$t = new streaminfo($argv[1]); // get metadata

/*
echo "Meta Interval:  ".$t->icymetaint;
echo "\n";
echo 'Current Track:  '.$t->streamtitle;
*/
?>

通过更新的代码,它会打印出头部和流标题信息的数组。如果你只想要当前播放的曲目,那么请注释掉两个print_r()语句,并取消注释末尾的echo语句。

#Example: run this command:
php getstreamtitle.php http://162.244.80.118:3066

#and the result is...

Array
(
    [0] => HTTP/1.0 200 OK
    [1] => icy-notice1:<BR>This stream requires <a href="http://www.winamp.com">Winamp</a><BR>
    [2] => icy-notice2:SHOUTcast DNAS/posix(linux x64) v2.6.0.750<BR>
    [3] => Accept-Ranges:none
    [4] => Access-Control-Allow-Origin:*
    [5] => Cache-Control:no-cache,no-store,must-revalidate,max-age=0
    [6] => Connection:close
    [7] => icy-name:
    [8] => icy-genre:Old Time Radio
    [9] => icy-br:24
    [10] => icy-sr:22050
    [11] => icy-url:http://horror-theatre.com
    [12] => icy-pub:1
    [13] => content-type:audio/mpeg
    [14] => icy-metaint:8192
    [15] => X-Clacks-Overhead:GNU Terry Pratchett
    [16] => 
)
StreamTitle='501026TooHotToLive';

这是使用Python和VLC的原始帖子

PHP解决方案一直在搜索,但从未为我返回响应。

虽然这不是所要求的PHP,但可能会帮助其他人寻找从实时流中提取“now_playing”信息的方法。

如果您只想要“now_playing”信息,则可以编辑脚本以返回该信息。

Python脚本使用VLC提取元数据(包括“now_playing”曲目)。您需要VLC和Python库:sys、telnetlib、os、time和socket。

#!/usr/bin/python
# coding: utf-8
import sys, telnetlib, os, time, socket

HOST = "localhost"
password = "admin"
port = "4212"

def check_port():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    res = sock.connect_ex((HOST, int(port)))
    sock.close()
    return res == 0

def checkstat():
  if not check_port(): 
    os.popen('vlc --no-audio --intf telnet --telnet-password admin --quiet 2>/dev/null &')
  while not check_port(): 
    time.sleep(.1)

def docmd(cmd):
  tn = telnetlib.Telnet(HOST, port)
  tn.read_until(b"Password: ")
  tn.write(password.encode('utf-8') + b"\n")
  tn.read_until(b"> ")
  tn.write(cmd.encode('utf-8') + b"\n")
  ans=tn.read_until(">".encode("utf-8"))[0:-3]
  return(ans)
  tn.close()

def nowplaying(playing):
  npstart=playing.find('now_playing')
  mystr=playing[npstart:]
  npend=mystr.find('\n')
  return mystr[:npend]

def metadata(playing):
  fstr='+----'
  mstart=playing.find(fstr)
  mend=playing.find(fstr,mstart+len(fstr))
  return playing[mstart:mend+len(fstr)]

checkstat()
docmd('add '+sys.argv[1])
playing=""
count=0
while not 'now_playing:' in playing:
  time.sleep(.5)
  playing=docmd('info')
  count+=1
  if count>9:
    break
if playing == "":
  print("--Timeout--")
else:
  print(metadata(playing))

docmd('shutdown')

例如,从Crypt Theater Station提取元数据:

./radiometatdata.py http://107.181.227.250:8026

响应:

+----[ Meta data ]
|
| title: *CRYPT THEATER*
| filename: 107.181.227.250:8026
| genre: Old Time Radio
| now_playing: CBS Radio Mystery Theatre - A Ghostly Game of Death
|
+----

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接