Создание плагина для Winamp
Данная статья расскажет вам о том, как можно создавать свои плагины для популярного мультимедиа плеера Winamp.
Вступление
В 2002 году я работал в одной компании системным администратором, и по долгу службы 80% времени мне приходилось находится в окружении серверов FreeBSD. У меня был еще в распоряжении был сервер с Windows на котором крутилась музыка чтобы не скучать, и было не удобно менять треки в плейлисте да и вообще работать с винампом (по некоторым причинам я не мог пользоваться такими вещами как Terminal Service, переключатели мониторов, и.т.п.), и я задался целью сделать управляющую программу для Винампа. Языком программирования был выбран VB, т.к. на этом языке я решения такой задачки не встречал и это мой любимый язык, также нужна была быстрота разработки.
Для программирования под API Winamp`а нам потребуется:
- Winamp SDK http://www.winamp.com/nsdn/winamp2x/dev/sdk/
- Visual Basic 5.0/6.0 желательно с установленным SP5
- Col_Rjl GenWrapper - Обвертка позволяющая использовать ActiveX DLL в Винампе http://www.winamp.com/nsdn/vault/GenWrapper.exe, http://www.winamp.com/nsdn/vault/WinAMP_VB.jhtml
- Немного API для работы с сообщениями Windows
Правда есть небольшое ограничение, этим набором можно создавать только основные (gen_*) плагины.
Данный текст рассчитан на программистов уже имеющих опыт работы в VB с сетевыми приложениями и WinAPI.
Часть 1. Пишем простую управляющую программу.
Для начала напишем простую программу (не плагин) для управления винампом. Например программу которая будет принимать команду на определенном TCP порту и транслировать ее Винампу.
Запускаем VB и создаем новый проект Standard EXE и добавляем в проект Microsoft Winsock Control 6.0, несколько API функций и констант, больше нам ничего не потребуется.
Вот декларации функций, которые нам понадобятся:
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" ( _ ByVal lpClassName As String, _ ByVal lpWindowName As String _ ) As Long
функция возвращает хендл на окно с заданным классом и/или строкой заголовка;
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _ ByVal hWnd As Long, _ ByVal wMsg As Long, _ ByVal wParam As Long, _ lParam As Any _ ) As Long
функция посылает сообщение окну в указанным хендлом;
Private Declare Sub Sleep Lib "kernel32" ( _ ByVal dwMilliseconds As Long _ )
функция позволяющая процессу (система не выделяет процессу процессорного времени) на указанное число миллисекунд;
Размещаем эти функции в секции General, также нам потребуются следующие управляющие сообщения винампа (полный их список есть в SDK к нему) и системы:
'system Private Const WM_USER = &H400 Private Const WM_COMMAND = &H111 'winamp Private Const WM_Raise_Volume = 40058 'increase 1% Private Const WM_Lower_Volume = 40059 'decrease 1% Private Const WM_Close_Winamp = 40001 Private Const WM_Previous = 40044 Private Const WM_Next = 40048 Private Const WM_Play = 40045 Private Const WM_Pause_Unpause = 40046 Private Const WM_Stop = 40047 Private Const WM_Toggle_Shuffle = 40023 Private Const WA_SETVOLUME = 122
Также добавляем пару переменных уровня формы:
Dim Response As String Dim Connections As Long
первая переменная потребуется для принятия строки от клиента, вторая для счета числа подключений. Внешними они сделаны по той причине, что они могут вам потребоваться в других методах, если нет, то переменную Response можно убрать в метод DataArrival.
Закончив с секцией General переходим к форме и основному коду.
Размещаем на форме Winsock и задаем ему имя wnsServer и устанавливаем его свойство Index = 0, в событие Form_Load пишем следующий код:
wnsServer(0).Protocol = sckTCPProtocol wnsServer(0).LocalPort = 806 wnsServer(0).Listen
Тут указываем что будем использовать только протокол TCP и указываем что для приема данных используем порт с номером 806.
Начинаем писать обработчики событий винсока,
первое - опишем процесс подключения клиента:
Private Sub wnsServer_ConnectionRequest(index As Integer, ByVal requestID As Long) If index = 0 Then Connections = Connections + 1 Load wnsServer(Connections) 'Load New control wnsServer(Connections).LocalPort = 0 wnsServer(Connections).Accept requested end if DoEvents End Sub
вот, мы приняли (принимает запросы только сокет с индексом 0) входящие соединение и выделили для его обслуживания отдельный сокет. При этом основной сокет может принять следующего клиента. LocalPort = 0 применено для того чтобы клиенту порт выделился динамически из числа свободных.
Ну и наконец ядро программы собственно обработка команд поступаемых в сокет:
Private Sub wnsServer_DataArrival(index As Integer, ByVal bytesTotal As Long) Dim hWnd As Long hWnd = FindWindow("Winamp v1.x", vbNullString) 'если к нам подконектились и если у нас присутствует 'винамп, ждем команду для отправки If bytesTotal 0 Then wnsServer(index).GetData Response 'получаем данные 'если нет винампа то можно только выходить If hWnd = 0 Then Exit Sub End If 'обработка поступившей команды 'Next Track If InStr(1, Response, "next", vbTextCompare) 0 Then SendMessage hWnd, WM_COMMAND, WM_Next, vbNull Exit Sub End If 'Previous Track If InStr(1, Response, "previous", vbTextCompare) 0 Then SendMessage hWnd, WM_COMMAND, WM_Previous, vbNull Exit Sub End If 'Play If InStr(1, Response, "play", vbTextCompare) 0 Then SendMessage hWnd, WM_COMMAND, WM_Play, vbNull Exit Sub End If 'Stop If InStr(1, Response, "stop", vbTextCompare) 0 Then SendMessage hWnd, WM_COMMAND, WM_Stop, vbNull Exit Sub End If 'Shuffle If InStr(1, Response, "shuffle", vbTextCompare) 0 Then SendMessage hWnd, WM_COMMAND, WM_Toggle_Shuffle, vbNull Exit Sub End If 'Pause/UnPause If InStr(1, Response, "pause", vbTextCompare) 0 Then SendMessage hWnd, WM_COMMAND, WM_Pause_Unpause, vbNull Exit Sub End If 'Close If InStr(1, Response, "close", vbTextCompare) 0 Then SendMessage hWnd, WM_COMMAND, WM_Close_Winamp, vbNull Exit Sub End If 'Volume inc If InStr(1, Response, "+", vbTextCompare) 0 Then If Response = "+" Then Response = "+1" Volume hWnd, CInt(Mid$(Response, InStr(1, Response, "+") + 1, 3)), 1 Exit Sub End If 'Volume dec If InStr(1, Response, "-", vbTextCompare) 0 Or InStr(1, Response, "0", vbTextCompare) 0 Then If Mid$(Response, InStr(1, Response, "-") + 1, 3) 'bytes End Sub
Метод проверяет загружен ли Винамп и если да то переходит к обработке пришедших данных.
Рассмотрим чуть подробнее один из блоков проверок приведенного кода:
If InStr(1, Response, "next", vbTextCompare) 0 Then SendMessage hWnd, WM_COMMAND, WM_Next, vbNull CloseSocket Index Exit Sub End If
собственно говоря это простейший вариант проверки поступивший команды и отправка сообщения Винампу. Параметры функции SendMessage: hWnd это хендл на окно винампа определенный в начале метода, WM_COMMAND - системное сообщение показывающие что в последующем параметре функции идет команда, WM_Next - собственно сама команда для окна винампа и последний параметр это дополнительные данные для Винампа или например параметры команды посылаемой окну. После отработки команды закроем сокет (для обеспечения сбалансированной нагрузки) и выйдем из метода.
Данный код является простейшей проверкой поступивших данных в сокет, в идеале нужно проверять соответствие команды определенному формату.
Ну и последние несколько вспомогательных методов:
Посылка команды увеличения или уменьшения (зависимости от параметра incdec) громкости.
Private Sub Volume(hWnd As Long, percent As Integer, incdec As Long) Dim i As Long For i = 0 To percent - 1 Select Case incdec Case -1 SendMessage hWnd, WM_COMMAND, WM_Lower_Volume, vbNull Case 1 SendMessage hWnd, WM_COMMAND, WM_Raise_Volume, vbNull End Select Next i End Sub
Событие происходит когда клиенту переданы все данные, как только оно возникает, выдерживаем интервал и закрываем сокет.
Private Sub wnsServer_SendComplete(index As Integer) Sleep 1000 CloseSocket index End Sub 'Само закрытие сокета и выгрузка его из памяти. Private Sub CloseSocket(index As Integer) wnsServer(index).Close Unload wnsServer(Connections) Connections = Connections - 1 DoEvents End Sub
Ну вот, теперь если все сделано без ошибок проект успешно откомпилируется и запустится сервер на ожидающий подключение на 806 порту, транслирующий команды Винампу. Для проверки его работы можно воспользоваться программкой TRCClient из каталога src, любители языка Perl могут воспользоваться управляющим скриптом от моего плагина VbTRC для Винампа.
Надеюсь это не стало для вас затруднением и на этом закончим первую часть нашей статьи.
Часть 2. От простой программы к настоящему плагину.
Ну вот мы освоились с простейшим управлением Винампом через TCP сокет, настало время создать настоящий плагин. Используем наши предыдущие исходники над которыми мы работали как шаблон.
Распаковываем скаченный GenWrapper.exe, оттуда нам понадобятся файлы GenWrapper.dll и GenWrapper.tlb, а также из каталога Template класс Plugin.cls. Создаем проект ActiveX DLL с именем tcpctrl удаляем из него Class1.cls и добавляем распакованный Plugin.cls. После добавления открываем пункт меню Project->References и добавляем ссылку на GenWrapper.tlb, не забыв также добавить компонент Microsoft Winsock Control. Ядро плагина мы создали, теперь мы можем использовать сокеты так как делали это в нашей первой программе, для любителей WinAPI скажу сразу что в данном случае лучше пользоваться сокетами напрямую через АПИ в этом случае можно будет отказаться от использования формы-контейнера.
Итак приступим к работе.
Создадим модуль main.bas, он нам понадобится для того чтобы корректно загрузить форму на которой будут располагаться наши элементы управления. Напрямую форму инициализировать нельзя, т.к. Винамп не поддерживает отображение форм на этапе своей загрузки и инициализации(даже когда она скрыта). В модуль поместим декларации АПИ функций из первой программы, а также добавим одну глобальную переменную Global This As Plugin (Где Plugin это имя нашего класса, его необходимо будет запомнить) для создания указателя на класс плагина.
Также в модуль добавляем следующий метод для загрузки нашей скрытой формы(ее параметры описываются ниже):
Public Sub ld() Load frmHidden End Sub
Открываем класс Plugin и следуем в метод IRjlWinAmpGenPlugin_Configure он вызывается при нажатии кнопочки Configure в диалоге настроек плагинов Винампа, т.к. в простейшем случае у нас параметров плагина нет, то просто выведем описание плагина: MsgBox App.FileDescription.
Следующий метод Info() вызывается при нажатии кнопочки "About" в диалоге настроек плагинов Винампа, тут может быть все что вам угодно я например вывожу такой MsgBox:
MsgBox "Plugin Description: " & vbCrLf & m_Wrapper.Description & vbCrLf & _ "WinAmp Window Handle: 0x" & Hex(m_Wrapper.HWndParent) _ , vbInformation, "tcpctrl Information"Метод заслуживающий отдельного внимания: IRjlWinAmpGenPlugin_Initialize он вызывается при загрузке винампа и инициализации его списка плагинов, в нем мы поменяем строчку - описание для списка найденных плагинов, например на такую: m_Wrapper.Description = "tcpctrl Plugin v." & App.Major & "." & App.Minor & "." & App.Revision & " (gen_tcpctrl.dll)". Как я уже и говорил напрямую Load frmHidden тут сделать нельзя из-за особенностей работы винампа, поэтому придется сделать косвенный вызов установив при этом ссылку на наш класс:
If Not This Is Nothing Then Err.Raise vbObjectError + 1, , "Already have a plugin instance" Exit Sub End If Set This = Me main.ld
все, форма загружена и инициализирована.
В методе IRjlWinAmpGenPlugin_Quit все просто, выгружаем нашу форму Unload frmHidden.
Вот и все, с классом мы закончили, приступим к созданию формы-контейнера для контролов. Добавляем форму в проект, даем ей имя frmHidden и устанавливаем ее свойство Visible равное False. Помещаем на нее Winsock с именем аналогичным как в первой программе, также помещаем сюда те же константы и переменные.
Событие Load формы будет выглядеть так:
Private Sub Form_Load() On Error Resume Next Me.Visible = False wnsServer(0).Protocol = sckTCPProtocol wnsServer(0).LocalPort = 806 wnsServer(0).Listen End SubКод для события Unload:
Private Sub Form_Unload(Cancel As Integer) Dim i As Long If Connections > 0 Then For i = Connections To 1 wnsServer(i).Close Unload wnsServer(Connections) Next i End If DoEvents End Sub
Код в данных местах практически идентичен коду в первом приложении, поступим также и с остальными методами, т.е можно просто скопировать следующие методы и функции: wnsServer_ConnectionRequest, wnsServer_DataArrival, Volume, wnsServer_SendComplete, СloseSocket.
Компилируем, надеюсь все прошло замечательно? Нет, тогда исправляем ошибки.
Теперь самое интересное, т.к. Винамп не понимает ActiveX DLL, то мы воспользовались обверткой Все поздравляю вы получили простейший рабочий плагин, а также необходимые знания и шаблоны для вашей дальнейшей деятельности.
Желаю удачи и творческих успехов.
P.S В своем плагине vbTRC я реализовал дополнительные функции управления Винампом такие как: работа с пультом ДУ от ТВ-Тюнера AverMedia(основные клавиши управления плюс любимые треки и предпрослушка треков), добавил также веб-интерфейс для управления и конфигурирования, простейшие списки доступа, автостарт после загрузки, запись NP и Uptime в файл для вставки в другие программы и другие разные улучшения и нововведения. Базовое ядро я использовал то же, что и приведено в данной статье плюс мои дополнения.
P.P.S На самом деле VB можно заставить делать настоящие не ActictiveX DLL которые Винамп поймет с легкостью. Но как всегда процесс этот весьма не прост и далеко не безглючен. Жалающие могут попробовать реализовать его на практике, буду рад если таким образом вы допишете 3ю часть статьи. Подробнее о создании простых DLL можно почитать вот в этой статье: http://www.fawcette.com/archives/listissue.asp?pubID=1&MagIssueId=215#.
Исходники проекта можно скачать здесь.