Получение информации о дорожке из аудиопотока с помощью PHP
Можно ли извлечь информацию о дорожке из аудиопотока с помощью PHP? Я провел некоторое копание, и ближайшая функция, которую я могу найти, - stream_get_transports, но мой хост не поддерживает транспорт http через fsockopen(), поэтому мне придется еще немного поработать, чтобы увидеть, что еще возвращает эта функция.
В настоящее время я пытаюсь получить метаданные исполнителя и отследить их из потока AOL.
5 ответов
Это поток SHOUTcast, и да, это возможно. Это никак не связано с тегами ID3. Недавно я написал сценарий для этого, но больше не могу его найти. Буквально на прошлой неделе я помог другому парню, у которого был довольно полный сценарий, сделать то же самое, но я не могу просто опубликовать источник, так как он не мой. Однако я свяжусь с ним, если вы напишите мне по адресу http://mailto%3Abrad@musatcha.com/.
В любом случае, вот как это сделать самостоятельно:
Первое, что вам нужно сделать, это подключиться к серверу напрямую. Не используйте HTTP. Ну, вы, вероятно, могли бы использовать cURL, но это, скорее всего, будет намного сложнее, чем стоит. Вы подключаетесь к нему с fsockopen()
( док). Убедитесь, что вы используете правильный порт. Также обратите внимание, что многие веб-хосты блокируют много портов, но обычно вы можете использовать порт 80. К счастью, все потоки SHOUTcast, размещенные на AOL, используют порт 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()
( док) заботится об этом для нас.
У вас останется что-то вроде этого:
StreamTitle='Awesome Trance Mix - DI.fm';StreamUrl=''
Я позволю вам выбрать ваш метод выбора для анализа этого. Лично я бы, наверное, просто разделил с ограничением 2 на ;
, но остерегайтесь названий, которые содержат ;
, Я не уверен, что такое метод escape-символа. Небольшой эксперимент должен помочь вам.
Не забудьте отключиться от сервера, когда закончите!
Существует множество ссылок на метаданные SHOUTcast. Это хороший: http://www.smackfu.com/stuff/programming/shoutcast.html
Проверьте это: https://gist.github.com/fracasula/5781710
Это небольшая суть функции PHP, которая позволяет извлекать метаданные MP3 (StreamTitle) из потокового URL.
Обычно потоковый сервер ставит 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));
Надеюсь, это поможет!
Большое спасибо за код 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;
}
Это код C# для получения метаданных с использованием HttpClient:
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;
}
ОБНОВЛЕНИЕ: Это обновление с более подходящим решением вопроса. Исходный пост также приведен ниже для информации.
Скрипт в этом посте, после некоторого исправления ошибок, работает и извлекает заголовок потока, используя PHP:PHP-скрипт для извлечения исполнителя и названия из потока Shoutcast/Icecast .
Мне пришлось внести пару изменений, потому что операторы эха в конце выдавали ошибку. Я добавил два оператора 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 извлекает метаданные (включая дорожку «now_playing») с помощью VLC. Вам понадобится 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
|
+----