Как слушать радио с использованием powershell и node.js

в 21:06, , рубрики: mp3, nodejs, powershell, proxy, ненормальное программирование, метки: , , ,
Часть первая: powershell и mci.

Используемая операционная система — Windows 7.
Первый вопрос, который возникает: как воспроизвести mp3 в powershell?
Ранее на хабре были упоминания подобного вопроса:
Как воспроизвести mp3 из командной строки в Windows 7?
Как же все-таки пользоваться PowerShell?

Ввиду отсутствия удовлетворительного ответа, было принято решение использовать Media Control Interface или mci.
Плюсы следующие: встроенность в систему, достаточная низкоуровневость, есть возможность посылать команды в виде строки.
Многим mci известен по ответу на вопрос: «как программно отрыть устройство чтения-записи компакт дисков?», хотя предоставляет много других возможностей. Ниже приведены некоторые сценарии использования.

Извлечение CD-ROM:

mci 'set cdaudio door open'

Воспроизведение локального mp3-файла:

mci 'play C:\temp\Kalimba.mp3'
mci 'status C:\temp\Kalimba.mp3 mode'

Запись в wav-файл:

mci 'open new type waveaudio alias RecWavFile'
mci 'set RecWavFile bitspersample 16 samplespersec 44100 channels 2'
mci 'record RecWavFile'
mci 'stop RecWavFile'
mci 'save RecWavFile C:\temp\RecWavFile.wav'
mci 'close RecWavFile'
mci 'play C:\temp\RecWavFile.wav wait'

Простой вариант воспроизведения потока из сети:

mci 'play http://some-radio-server.com:80/some-radio-channel.mp3'


Продвинутый вариант воспроизведения потока из сети:

mci 'open http://some-radio-server.com:80/some-radio-channel.mp3 type mpegvideo alias radio'
#mci 'open http://other-radio-server/other-radio-channel.fake.mp3 type mpegvideo alias radio'
mci 'status radio volume'
mci 'setaudio radio volume to 100'
mci 'play radio'
mci 'status radio mode'
mci 'stop radio'
mci 'close radio'

Может показаться странным, но изначально в powershell нет подобного способа(автор не нашел) работы с mci. Чтобы сделать эту работу возможной, требуется написать обертку над функцией mciSendStringA. При этом, чтобы иметь полноценный command line interface, помимо ввода команд, требуется вывод результата выполнения функции и расшифровка кодов ошибок. Первое необходимо, например, для запроса состояния командой status. Второе удобно, так как не нужно каждый раз искать описание ошибки.

mci powershell wrapper:

$MemDef =@"
[DllImport("winmm.dll", CharSet = CharSet.Ansi)]
public static extern int mciSendStringA(
string lpstrCommand,
[Out] byte[] lpstrReturnString,
int uReturnLength,
IntPtr hwndCallback);

[DllImport("winmm.dll", CharSet = CharSet.Ansi)]
public static extern int mciGetErrorStringA(
int dwError,
[Out] byte[] lpstrBuffer,
int uLength);
"@
$winmm = Add-Type -memberDefinition $MemDef -ErrorAction 'SilentlyContinue' -passthru -name mciSendString
function mci($strSend)
{
    $buffer = New-Object byte[] 128
    [int]$error = $winmm::mciSendStringA( $strSend, $buffer, 128, 0 )
    [Text.Encoding]::ASCII.GetString($buffer)
    $error_buffer = New-Object byte[] 128
    [void]$winmm::mciGetErrorStringA( $error, $error_buffer, 128 )
    [Text.Encoding]::GetEncoding('windows-1251').GetString($error_buffer)
}

Возможно, будут полезными некоторые пояснения.
Внутри $MemDef содержится кусок кода на С#. А внутри $winmm — уже результат его компиляции.
Для получения текстовой строки нужно из библиотечной функции:

  1. объявить тип возвращаемой строки(в коде на С#, а значит, регистр имеет значение) как [Out] byte[];
  2. при вызове подготовить буфер;
  3. передать буфер и размер буфера;
  4. интерпретировать полученные байты как строку в определенной кодировке.

Может показаться, что использование включений C#, тем более для объявления winapi-шных функций, — не очень правильно. Но тут важно именно удобство использования, а не способ его достижения: строка с командой mci гармонично вливается в стиль powershell.

Список mci команд можно найти тут или поиском «Multimedia Command Strings».
P.S. Через mci можно и фильмы смотреть.

Часть вторая: http proxy и node.js

Как видно из примеров: есть возможность указывать в качестве имени не только локальные файлы, но и URL.
Но для некоторых серверов(таких, как other-radio-server) существует проблема: воспроизведение начинается только после принудительного разрыва соединения. Тут может помочь собственный HTTP proxy «исправляющий» «неправильные» пакеты. В частности, для немедленного воспроизведения достаточно добавлять в ответ заголовок 'content-length'.
Для написания своего прокси лучше всего подойдет node.js.

После запуска и подключения прокси можно использовать закомментированную строчку вместо предшествующей ей.
Окончание url ".fake.mp3" говорит о необходимости проведения манипуляций со стороны прокси. Ниже приведен далекий от оптимального код сервера.

node/proxy_server.js:

require('./simple_cli').run()
var http = require('http')
var url = require('url')

http.createServer(function(request, response) {
  var u = url.parse(request.url)
  var options = {
    port     : u['port'] || 80,
    host     : u['hostname'],
    method   : request.method,
    path     : u['pathname'],
    headers  : request.headers
  }
  console.log(request.url)
  delete options.headers['proxy-connection']
  if( request.url.substr(-9) === ".fake.mp3" )
  {
    var p = options['path']
    options['path'] = p.substr(0, p.length - 9)
    options['icy-metadata'] = '0'
  }
  console.log(options)
  var proxy_request = http.request(options);
  proxy_request.on('response', function (proxy_response) {
    console.log(proxy_response.headers)
    if( request.url.substr(-9) === ".fake.mp3" )
    {
      proxy_response.headers['content-length'] = '221183499'
    }
    proxy_response.pipe(response);
    response.writeHead(proxy_response.statusCode, proxy_response.headers);
  });
  request.pipe(proxy_request);
}).listen(8080);

console.log('Server running...');

P.S. Прокси может также пригодиться, например, для просмотра или предобработки отправляемых запросов и принимаемых ответов.

Часть третья: интерактивность

Иногда, для удобства просмотра заголовков, проходящих через сервер, нужно очистить консоль. Простейший интерфейс командной строки позволяет этого добиться.
node/simple_cli.js

module.exports.run = function(){
  process.stdin.resume();
  process.stdin.setEncoding('utf8');
  process.stdin.on('data', function (data) {
    data = data.toString().trim();
    switch (data)
    {
    case 'exit':
      process.exit();
      break;
    case 'clear':
      process.stdout.write('u001B[2Ju001B[0;0f');
      break;
    case 'help':
      process.stdout.write('exit, clear, helpn');
      break;
    default:
      process.stdout.write('unrecognize: '+data+'n');
    }
  });
}

Это нужно для удобного запуска сервера:
run_proxy_server.cmd

"%~d0%~p0node.exe" "%~d0%~p0nodeproxy_server.js"
pause

Итоговый рецепт:

  1. скачиваем в рабочую папку node.exe c «nodejs.org/download/»
    После создания в ней всех файлов, получаем следующее дерево:
    +
    |-node.exe
    |-run_proxy_server.cmd
    |+node
    | |-proxy_server.js
    | |-simple_cli.js
  2. запускаем http proxy:
    run_proxy_server.cmd
  3. задаем локальный прокси:
    windows+R, control, ctrl+F, pr
    «Настрока прокси сервера»,«Настрока сети»,«Прокси сервер»,«Использовать прокси-сервер....»
    Адрес: 127.0.0.1 Порт: 8080
    Ок
  4. интерактивно исполняем код обертки
  5. интерактивно исполняем примеры

Для последних двух пунктов можно использовать, например, любой из вариантов:

  • poweshell из коммандной строки
  • Windows PowerShell
  • Windows PowerShell ISE

P.S В качестве some-radio-server и some-radio-channel можно взять, например, icy-e-05.sharp-stream и kiss100low соответственно, а в качестве other-radio-server и other-radio-channel — pub5.di.fm и di_eurodance.

Автор: dtestyk

Источник

Поделиться

* - обязательные к заполнению поля