Статьи‎ > ‎Фильтры‎ > ‎

Передача данных в графе с точки зрения пользователя

Передача данных в графе фильтров с точки зрения пользователя

Давайте рассмотрим вопрос о прохождении данных через граф фильтров. Для написании конечных DirectShow-приложений не обязательно иметь полное представление об этом процессе, хотя и могут быть ситуации, в которых такие знания оказались бы полезными. Но для написания фильтров нужно хорошее понимание этого материала. Итак:

Общий обзор

Здесь мы рассмотрим движение данных в DirectShow крупным планом. Детальные же описания разнообразных моментов будут рассмотрены в других разделах.

Данные хранятся в буферах, которые есть простыми массивами данных. Каждый буфер есть оболочкой над COM-объектом, называемой порцией медиаданных (media sample), которая реализует интерфейс IMediaSample. Порции данных создаются другими типами объектов, которые называются распределителями (allocator) и реализуют интерфейс IMemAllocator. Распределитель привязан к каждому соединительному контакту, хотя два или больше соединительных контакта могут совместно использовать один распределитель.

Рис. 6. Распределители и порции данных

Каждый распределитель создает совокупность (pool) порций медиаданных и выделяет буфера для каждой порции данных. Всегда, когда фильтру бывает необходимо залить буфер данными, он запрашивает порцию данных у распределителя посредством вызова IMemAllocator::GetBuffer. Если распределитель имеет данные, не использующиеся другим фильтром, метод GetBuffer тут же возвращает указатель на порцию данных. Если же все порции данных этого распределителя уже кем-то используются, метод блокируется до тех пор, пока данные не станут доступными. Когда метод возвращает порцию данных, фильтр кладет данные в буфер, устанавливает соответствующие флаги для порции данных (скажем, временные метки в том числе) и переправляет данные вниз по течению (следующему фильру т.е. - downstream).

Когда фильтр рендеринга получает порцию данных, он проверяет временные метки и удерживает данные до тех пор, пока ссылочные часы графа фильтров не укажут, что данные можно воспроизводить. После воспроизведения порции данных она может быть освобождена. Порция данных не возвращается назад в пул распределителя данных до тех пор, пока счетчик ссылок не станет равным нулю, что будет означать, что каждый фильтр освободил эту порцию (НЕ СОВСЕМ ПОНЯЛ НИ СМЫСЛА И, СООТВЕТСТВЕННО, НЕ УВЕРЕН, ЧТО ПРАВИЛЕН ПЕРЕВОД: The sample does not go back into the allocator's pool of samples until the sample's reference count is zero, meaning that every filter has released the sample).

Рис. 7. Передача данных в графе фильтров

Фильтр, находящийся до рендерера, может работать быстрее - в том случае, когда он заливает свой буфер бысрее, чем происходит воспроизведение, т.к., например, буфера заливаются быстро, а рендерер выдерживает данные до их времени показа. Больше того, фильтр, предваряющий рендерер, не сможет переписать буфера даже случайно, поскольку метод GetSample возвращает только те порции данных, которые никем не используются. Количество порций данных, которые может удерживать этот фильтр, определяется количеством порций данных пула распределителя. (ВОТ ЭТО ТОЖЕ НЕЯСНО)

Предыдущий рисунок показывает только один распределитель, но обычно один поток имеет несколько распределителей. Так, когда рендерер освобождает данные, это может иметь каскадный эффект. Следующий рисунок показывает ситуацию, когда декодер удерживает сжатый видеофрейм пока производится ожидание освобождения данных рендерером. Фильтр разбора также ждет момента, когда декодер, в свою очередь, освободит данные.

Рис. 8. Передача данных в графе фильтров

Когда рендерер освобождает, наконец, данные, декодер получает, наконец, ответ на свой вызов GetBuffer. Тогда декодер сможет разжать сжатый видеокадр и освободить удерживаемую им порцию данных, разблокировав тем самым вызов разборщиком метода GetBuffer.

Транспортировка

Для поддержки движения медиаданных через граф фильтров DirectShow-фильтр должен поддерживать один из нескольких возможных протоколов. Эти протоколы называются транспортами. Если два фильтра соединены, они должны поддерживать одинаковый транспорт; в противном случае они не смогут обмениваться медиаданными. Обычно транспорт требует, чтобы один из контактов поддерживал специфический интерфейс. Когда фильтры соединяются, один контакт запрашивает другой на его наличие (НИЧЕ НЕ ПОНЯЛ: ЧТО ЗА СПЕЦИФИЧЕСКИЙ ИНТЕРФЕЙС. НАВЕРНОЕ, СНОВА ПЕРЕВОД ЛИПОВЫЙ.).

Большинство фильтров DirectShow удерживают медиаданные в главной памяти и пересылают их другим фильтрам чере соединения контактов. Такой тип транспорта называется транспортом локальной памяти (local memory). Хотя этот тип транспорта наиболее общ для DirectShow, не все фильтры поддерживают его. Например, некоторые фильтры пересылают данные аппаратным путем и используют контакты только для пересылки контрольной информации. Это используется, например, в интерфейсе IOverlay.

DirectShow определяет два механизма для транспорта локальной памяти - это push и pull модели. В push-модели фильтры источника продуцирует данные и передает их в следующий фильтр через нисходящий поток. Этот фильтр пассивно получает данные, обрабатывает их и пересылает дальнейшим нисходящим потоком. В pull-модели фильтр источника соединен с фильтром анализа. Анализатор затребывает данные у фильтра источника. Фильтр-источник отвечает на запрос пересылкой данных. Push-модель использует интерфейс IMemInputPin, а pull-модель - интерфейс IAsyncReader.

Push-модель более обща, чем pull. Поэтому обычно будет предполагаться, если не указано противное, что используется push-модель. Впрочем, в разделе Pull-модель будет описано использование интерфейса IAsyncReader и его отличие от IMemInputPin.

Порции данных и распределители памяти

Когда контакт передает порцию данных другому контакту, он не передает непосредственно указатель на соответствующий буфер в памяти. Вместо этого он передает указатель на COM-объект, управляющий памятью. Такой объект, называемый медиаданными (media sample), предоставляет интерфейс IMediaSample. Получение контактом доступа к буферу в памяти производится через вызовы таких методов, как IMediaSample::GetPointer, IMediaSample::GetSize, IMediaSample::GetActualDataLength.

Порции данных всегда проходят вниз по потоку, от исходящего контакта к входящему контакту. В push-модели исходящий контакт передает данные через вызов IMemInputPin::Receive на входящий контакт. Входящий контакт или обрабатывает данные синхронно (так реализовано внутри метода Receive), или обрабатывает их асинхронно на рабочем потоке. Входящий контакт можно заблокировать внутри метода Receive, если это необходимо для ожидания ресурсов.

Другой COM-объект - распределитель, отвечает за создание и манипуляцию порциями медиаданных. Распределители предоставляют интерфейс IMemAllocator. Всякий раз, когда фильтру необходимы медиаданные с пустым буфером, он вызывает метод IMemAllocator::GetBuffer, который возвращает указатель на порцию данных. Каждое контактное соединение совместно использует один распределитель. Когда соединены два контакта, они решают, какой фильтр предоставит распределитель памяти. Контакты также устанавливают свойства распределителя, такие, как количество буферов и размер каждого буфера. (См. также Как соединяются фильтры и Договор о распределителях памяти.)

Следующая картинка показывает, как происходит взаимодействие между распределителем памяти, медиаданными и фильтром.

Рис. 9. Взаимодействие распределителей памяти, медиаданными и фильтром

Рассмотрим вопрос о счетчике ссылок порций медиаданных.

Распределитель создает конечный пул порций. В любой момент времени некоторый из этих порций могут использоваться, тогда как другие будут доступны при вызовах GetBuffer. Распределитель использует счетчик ссылок для слежения за порцией данных. Метод GetBuffer возвратит порцию данных с установкой счетчика ссылок в 1. Если счетчик ссылок сравняется с нулем, порция данных вернется назад в пул распределителя, и сможет быть использован для следующего вызова GetBuffer. Как только счетчик ссылок становится больше нуля, эта порция более недоступна для вызова GetBuffer. Если каждая порция данных используется, метод GetBuffer блокируется до тех пор, пока некоторые порции данных не становятся доступными.

Предположим, например, что входящий контакт получает порцию данных. Если обработка данных выполняется синхронно, внутри метода Receive, счетчик ссылок не увеличивается. После того, как вызов Receive вернет управление, исходящий контакт освободит данные, счетчик ссылок установится в ноль, и порция данных вернется в пул распределителя. С другой стороны, если входящий контакт обрабатывает порцию данных на рабочем потоке, он увеличивает на единицу счетчик ссылок до окончания возврата метода Receive. Счетчик ссылок станет теперь равным 2. Когда эту порцию исходящий контакт освободит, счетчик станет равным 1, и порция еще не будет возвращена в пул. После отработки рабочего потока будет вызвана функция Release для освобождения этой порции. И теперь она будет возвращена пулу.

Когда контакт получает данные, он копирует их в другое место или модифицирует исходные данные и передает их следующему фильтру. Потенциально порция данных может пройти граф по всей длине, каждый фильтр будет вызывать AddRef и Release при возврате. Т.о., исходящий контакт не сможет использовать ту же порцию данных после вызова Receive, потому что далее идущие фильтры могут продолжать ее использовать. Исходящий контакт должен всегда использовать вызов GetBuffer для получения памяти под новые данные.

Этот механизм уменьшает количество выделяемой памяти, т.к. фильтры каждый раз используют те же буфера. Это также предупреждает случайную перезапись фильтрами еще необработанных данных, т.к. распределители работают со списками доступных порций данных.

Фильтр может использовать отдельные распределители для входа и выхода. Это есть смысл делать, если объем данным на исходящем контакте может быть больше, чем на входящем (например, при разжатии). Если количество исходящих данных не больше входящих, фильтр может обрабатывать их по месту (in place), без копирования их в новый сэмпл. В этом случае два или больше контактных соединений могут совместно использовать один и тот же распределитель.

Когда фильтр создает распределитель, он не резервирует никаких буферов в памяти. В этот момент любой вызов GetBuffer будет приводить к ошибке. Когда поток стартует, исходящий контакт вызывает метод IMemAllocator::Commit, который подтверждает (commits) распределитель и тогда память может быть выделена. Теперь контакты могут вызывать GetBuffer.

Когда поток останавливается, контакт вызывает метод IMemAllocator::Decommit, который отменяет (decommit) действительность распределителя. Все последующие вызовы GetBuffer будут ошибочны до тех пор, пока распределитель не будет вновь подтвержден. Если, также, какой-либо вызов GetBuffer уже заблокировал сэмпл, другие вызовы GetBuffer будут немедленно возвращать ошибочный результат. Метод Decommit может как освобождать, так и не освобождать память - это зависит от реализации. Например, класс CMemAllocator освобождает память в деструкторе.

Состояния фильтра.

Фильтр может находиться в трех возможных состояниях: остановленном (stopped), приостановленом (paused) и запущенном (running). Замыслом приостановленного состояния есть передача передача данных в граф, так что команда на запуск отрабатывает немедленно. Менеджер графа фильтров управляет всеми состояниями переходов. Когда приложение вызывает методы IMediaControl::Run, IMediaControl::Pause или IMediaControl::Stop, менеджер графа фильтров вызывает соответствующий IMediaFilter'у-метод для всех фильтров. Переход от остановки к запуску всегда проходит через состояние паузы, так что если приложение вызовет метод Run на остановленном графе, менеджер графа фильтров приостановит граф перед его запуском.

Для большинства фильтров запущенный и приостановленный состояния - идентичны. Рассмотрим следующий граф фильтров:

Источник > Преобразователь > Рендерер

Предположим теперь, что фильтр источника - это фильтр не живого источника. Когда фильтр-источник приостанавливается, создается нить (thread), который продуцирует новые данные и записывает их в медиасэмпл так быстро, как это возможно. Нить "проталкивает" сэмпл дальше по потоку, используя вызов IMemInputPin::Receive на входящий контакт фильтра-преобразователя. Фильтр-преобразователь получает сэмпл на нити фильтра-источника. Он может использовать рабочую нить для передачи сэмпла рендереру, но, обычно, он передает его на том же потоке. До тех пор, пока рендерер находится в приостановленном состоянии, он ожидает получения сэмпла. После его получения он блокирует и удерживает этот сэмпл. Если это видеорендерер, он показывает этот сэмпл как картинку, перерисовывая ее в случае необходимости.

В этом состоянии поток вполне жив и готов к рендерингу. Если граф продолжает пребывать в паузе, сэмплы будут "накапливаться в запасе" в графе, пока все фильтры не будут заблокированы вызовами в Receive или в GetBuffer. Из-за этого никакие данные не будут потеряны. Когда нить источника разблокируется, данные будет просто возвращаться с той точки, где они были заблокированы.

Фильтр-источник и фильтр-преобразователь игнорируют переход от приостановки к запуску - они просто продолжают обрабатывать данные так быстро, как это возможно. Но когда рендерер запускается, он начинает воспроизводить сэмплы. Сначала он воспроизводит сэмпл, который удерживался им в момент наступления команды о паузе. Затем он начинает получать все новые и новые сэмплы, учитывая их время показа (presentation time). Рендерер показывает сэмпл на протяжении времени его отображения. Пока ожидается время презентации, они блокируются в методе Receive или получают новые сэмплы на рабочей нити в порядке очереди. Вышележащие относительно рендерера фильтры не включаются в ... (НЕ ПОНЯЛ: Filters upstream from the renderer are not involved in scheduling.)

Живые источники, такие, как устройства захвата - исключение из этой общей архитектуры. Для живых источников не подходит накапливание будущих данных. Приложение может приостановить граф и длительное время не стартовать его. И граф не должен будет показывать устаревшие данные. Т.о., живой источник не выдает никаких сэмплов во время паузы, а только тогда, когда запущен. Для сообщения этого факта менеджеру графа фильтров метод фильтра-источника IMediaFilter::GetState возвращает VFW_S_CANT_CUE. Этот код возврата сообщает о том, что фильтр переключен в приостановленный режим, в котором рендерер не может получить никаких данных.

Когда фильтр остановлен, он отклоняет любые сэмлы, поступающие к нему. Фильтр-источник закрывает свою потоковую нить, и другие фильтры прекращают все рабочие потоки, которые были созданы. Контакты отменяют (decommit) свои распределители.

Менеджер графа фильтров проносит все изменения состояний снизу вверх, начиная от рендерера и заканчивая фильтром источника. Такой порядок необходим для предупреждения потери сэмплов и предупреждения взаимоблокировки. Наиболее критично состояние перехода между паузой и остановкой:

  • Остановка -> Пауза. Когда все фильтры приостановлены, они готовы получать данные от следующих фильтров. Фильтр источника приостанавливается последним. Он создает потоковую нить и начинает передавать сэмплы. Т.к. все нижележащие фильтры приостановлены, фильтр будет отвергать любые сэмплы. Менеджер графа фильтров не завершает передачу до тех пор, пока все рендереры в графе не получат сэмплы (за исключением живых источников, ка было описано выше).
  • Пауза -> Остановка. Когда фильтр останавливается, он освобождает все сэмплы, которые он удерживал, разблокируя тем самым все вышележащие фильтры, ожидающие GetBuffer. Если фильтр ожидает ресурс внутри метода Receive, он прекращает ожидание и возвращается из Receive, разблокируя тем самым вызываемый фильтр. Т.о., когда менеджер графа фильтров останавливает следующий вышележащий фильтр, этот фильтр не блокируется никакими GetBuffer и Receive и может ответить на команду остановки. Вышележащий фильтр может передать несколько дополнительных сэмплов перед получением команды остановки, но нижележащие фильтры просто отвергнут их, т.к. они уже остановлены.

Pull-модель.

В интерфейсе IMemInputPin вышерасположенный фильтр определяет данные для отправки и проталкивает их далее нижележащему фильтру. Для некоторых фильтров больше подходит pull-модель. В ней нижележащий фильтр запрашивает данные у вышележащего. Сэмплы идут сверху вниз, но пересылку данных инициирует нижележащий фильтр. Этот тип соединения использует интерфейс IAsyncReader.

Типичное использование pull-модели - проигрывание файла. Например, для графа AVI-проигывания фильтр Async File Source выполняет операцииобщего чтения и передает данные в виде байтового без какой бы то ни было форматирующей информации. Фильтр AVI Splitter зачитывает AVI-заголовки и разбирает этот поток на аудио- и видео- сэмплы. AVI Splitter лучше понимает суть данных, чем Async File Source, и поэтому используется интерфейс IAsyncReader вместо IMemInputPin.

Для запроса данных у исходящего контакта входящий контакт вызывает один из следующих методов:

  • IAsyncReader::Request
  • IAsyncReader::SyncRead
  • IAsyncReader::SyncReadAligned

Первый метод асинхронный поддерживает множественные перекрывающиеся операции чтения. Остальные методы - синхронные.

Теоретически любой фильтр может поддерживать интерфейс IAsyncReader, но на практике он разрабатывается для фильтров-источников, которые соединяются с фильтрами разбора. Действия парсера очень похожи на действия фильтра-источника в push-модели. В случае приостановки (paused) создается потоковая нить, которая забирает данные у IAsyncReader-соединения и проталкивает их дальше, вниз по графу. Исходящий контакт использует интерфейс IMemInputPin и остальная часть графа использует стандартную push-модель.

Comments