【问题标题】:Pulling Track Info From an Audio Stream Using PHP使用 PHP 从音频流中提取音轨信息
【发布时间】:2011-06-22 02:47:04
【问题描述】:

是否可以使用 PHP 从音频流中提取音轨信息?我已经进行了一些挖掘,我能找到的最接近的函数是 stream_get_transports 但我的主机不支持通过 fsockopen() 进行的 http 传输,所以我必须做更多的修改才能看到该函数还返回什么。

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

【问题讨论】:

  • scfire-dtc-aa01.stream.aol.com:80/stream/1003。特别是那个 URL,但我正在从数字导入的播放列表(pls 文件)中提取我的数据。
  • 我想我在 phpclasses.org 上看到了一些用于 mp3 标签元信息等的音频处理类。看一看。有很多好东西,如果不知道你想要什么数据,很难推荐具体的东西。

标签: php audio-streaming


【解决方案1】:

这是一个 SHOUTcast 流,是的,它是可能的。它与 ID3 标签完全无关。我前段时间写了一个脚本来做到这一点,但现在找不到了。就在上周,我帮助了另一个拥有相当完整脚本的人来做同样的事情,但我不能只发布源代码,因为它不是我的。不过,如果你给我发电子邮件 brad@musatcha.com,我会让你与他联系。

不管怎样,下面是自己动手的方法:

您需要做的第一件事是直接连接到服务器。不要使用 HTTP。好吧,您可能可以使用 cURL,但它可能比它的价值要麻烦得多。您可以使用fsockopen() (doc) 连接到它。确保使用正确的端口。另请注意,许多 Web 主机会阻塞很多端口,但您通常可以使用端口 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=''

我会让你选择你选择的方法来解析这个。就我个人而言,我可能只是在; 上拆分为 2,但要注意包含; 的标题。我不确定转义字符方法是什么。一些小实验应该会对您有所帮助。

完成后不要忘记断开与服务器的连接!

那里有很多 SHOUTcast MetaData 参考。这个不错:http://www.smackfu.com/stuff/programming/shoutcast.html

【讨论】:

  • 你应该注意'而不是;
  • 您是否知道 (a) 使用什么编码(UTF-8、ISO-8859、...?),以及 (b) 标题中是否/如何使用 ' 字符还是 URL 被转义了?
  • @Thomas 它各不相同。一些电台使用 ISO-8859,而如今许多电台使用 UTF-8。客户支持也各不相同。与转义相同...某些频道将使用反斜杠 `` 来转义撇号。有些人会加倍。不幸的是,这方面没有真正的标准。
【解决方案2】:

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

这是一个 PHP 函数的小要点,可让您从流式 URL 中提取 MP3 元数据 (StreamTitle)。

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

否则,该函数会根据您的时间间隔调用流式 URL,如果不存在任何元数据,则它会通过从 offset 参数开始的递归再次尝试。

<?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));

我希望这会有所帮助!

【讨论】:

  • 解析错误:语法错误,第 8 行 C:\wamp\www\stream\index.php 中的意外 '['
  • 您必须至少使用 PHP 5.4。否则,请尝试使用经典的 array 语法转换 $opts 数组。
  • 这行得通,但要花很多时间才能得到结果。如何优化?
【解决方案3】:

非常感谢代码 fra_casula。这是一个在 PHP

由于自己的需要,我删除了异常,如果没有找到,则返回 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;
    }

【讨论】:

    【解决方案4】:

    这是使用 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 中提取元数据,还是随着流的进行不断更新诸如 Artist 等参数?
    【解决方案5】:

    更新: 这是对问题的更合适解决方案的更新。下面还提供了原始帖子以供参考。

    这篇文章中的脚本,经过一些错误更正后,使用 PHP 工作并提取流标题: PHP script to extract artist & title from Shoutcast/Icecast stream.

    我不得不进行一些更改,因为最后的 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;
    */
    ?>
    

    使用更新的代码,它会打印标题和流标题信息的数组。如果只想要 now_playing 曲目,则注释掉这两个 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 Theatre 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
    |
    +----
    

    【讨论】:

      猜你喜欢
      • 2010-09-20
      • 2020-12-19
      • 2012-06-26
      • 2010-10-09
      • 2016-01-23
      • 2016-03-22
      • 1970-01-01
      • 1970-01-01
      • 2015-08-30
      相关资源
      最近更新 更多