История создания

Vitaly Vylgin edited this page Jul 2, 2014 · 45 revisions

I Задание

Реализовать медиа проигрыватель для проигрывания Http Dynamic Streaming контента .f4m.

  • базовый фреймворк Open Source Media Framework
  • пакет Flex SDK
  • базовый размер 640 x 360
  • поддрежка FullScreen режима
  • Обязательные элементы управления: кнопки Play/Pause, Stop; Timeline; FullScreen; элемент управления для отображения и изменения качества; текущее время и общее время.
  • поддержка перемещений (seek) по контенту.
  • отображение текущей позиции проигрывания на Timeline.

Письмо с текстом тестового задания пришло мне в конце лета 2013 года. До этого времени я только один раз имел дело с языком программирования actionscript, когда создавал игру SlotMachine, так что я сразу понял, что предстоит много времени провести в изучении OSFM и Flex SDK. Прошел день с момента получения письма с заданием, как я узнал об успешном прохождении собеседования на должность java-программиста в другой фирме. К этому времени я уже приступил к изучению фреймворка OSMF и мне не хотелось бросать этот интересный проект, поэтому я решил его доделать в свободное время и попросил оценить конечный результат людей, которые прислали задание.

II Выполнение

Содержание главы:

1. Изучение OSMF

Изучение OSMF шло с помощью документа "OSMF Release Samples", который доступен на официальном сайте, он дает всю необходимую информацию для начала работы. Главное выделять те моменты, которые наилучшим образом подходят для решения поставленной задачи.

2. Изучение FlexSDK

Я создал шаблонный проект ActionScript+Flex в IntelliJ IDEA, который и стал отправной точкой для ознакомления. Беглого взгляда хватило, чтобы понять, что он из себя представляет на поверхностном уровне. У меня он вызывал ассоциации с разработкой под android, если использовать xml документы для создания разметки Activities, Fragments и прочей кастомизации Views, или с разработкой на Qt, QML и C++, вот только xml по декларативности уступает QML, но это не критично. Но одного шаблона было недостаточно, поэтому у меня была всегда открыта вкладка с документацией.

3. Структура проекта

  • FlexOSMFPlayer.xml
    • Разметка UI
    • Ссылка на объект класса VideoDisplay
  • VideoDisplay.as
    • Содержит
      • Классы OSMF
        • MediaPlayer
        • MediaContainer
        • MediaElement
        • DefaultMediaFactory
      • Обработчики событий класса MediaPlayer
        • DisplayObjectEvent.MEDIA_SIZE_CHANGE
        • TimeEvent.DURATION_CHANGE
        • TimeEvent.CURRENT_TIME_CHANGE
        • TimeEvent.COMPLETE
        • DynamicStreamEvent.SWITCHING_CHANGE
  • html-template
    • Html шаблон, созданный intellij idea

4. Воспроизведение видео без элементов управления

Промежуточной задачей к достижению цели, было воспроизведение видео без добавления элементов управления. Для этого в файле FlexOSMFPlayer.xml появились строчки:

<s:Application  xmlns:fx="http://ns.adobe.com/mxml/2009"
           xmlns:s="library://ns.adobe.com/flex/spark"
           xmlns:mx="library://ns.adobe.com/flex/mx" 
           minWidth="640" 
           minHeight="360" 
           xmlns:flexosmf="flexosmf.*">
	...
    <fx:Script>
        <![CDATA[
            private const HTTP:String = "http://mediapm.edgesuite.net/osmf/content/test/manifest-files/dynamic_Streaming.f4m";
        ]]>
    </fx:Script>
    ...
    <flexosmf:VideoDisplay
            id = "videoDisplay"
            source= "{HTTP}"/>
    ...
</s:Application>

В класс VideoDisplay были добавлены приватные поля, отвечающие за воспроизведение видео:

private var mediaPlayer:MediaPlayer;
private var container:MediaContainer;
private var localSource:String;
private var element:MediaElement;
private var mediaFactory:DefaultMediaFactory;

Были переопределены методы createChildren и mesure:

override protected function createChildren():void {
    super.createChildren();

    mediaFactory = new DefaultMediaFactory();

    container = new MediaContainer();

    addChild(container);

    if(localSource) {
        playMedia();
    }
}

override protected function measure():void {
    super.measure();

    measuredWidth = mediaPlayer.mediaWidth;
    measuredHeight = mediaPlayer.mediaHeight;
    measuredMinWidth = measuredMinHeight = 5;
}

Были добавлены методы playMedia и onMediaSizeChange:

public function playMedia():void {
    if(mediaPlayer) {
        if(element) {
            if(container.containsMediaElement(element)) {
                container.removeMediaElement(element);
            }
        }
        element = mediaFactory.createMediaElement(new URLResource(localSource));
        mediaPlayer.media = element;
        container.addMediaElement(element);
    }
}

protected function onMediaSizeChange(e:DisplayObjectEvent):void {
    e.stopPropagation();
    invalidateSize();
    invalidateDisplayList();
}

В теге <flexosmf:VideoDisplay/> неявно вызывается сеттер для приватного поля source класса VideoDisplay source= "{HTTP}", поэтому при запуске приложения сразу проигрывается видео, в котором нельзя изменить время воспроизведения, громкость и качество. Меняется только размер плеера, в зависимости от размера родительского экрана.

5. Добавление кнопок "Play/Pause" и "Stop"

Кнопки "Play/Pause" и "Stop"

Как оказалось, добавить кнопки, отвечающие за воспроизведение видео не так сложно, достаточно было добавить в файл разметки FlexOSMFPlayer.xml соответствующие теги ToggleButton и Button:

<s:ToggleButton
        id = "playPauseButton"
        label = "Pause"
        click = "playPauseEvent(event)"/>

<s:Button
        label = "Stop"
        click = "stop()"/>

И функции обработки нажатий на кнопки:

private function playPauseEvent(event:MouseEvent):void {
    if (event.target.selected) {
        videoDisplay.pause();
        playPauseButton.label = "Play";
    } else {
        videoDisplay.play();
        playPauseButton.label = "Pause";
    }
}

private function stop():void {
    videoDisplay.stop();
    playPauseButton.label = "Play";
}

И соответствующие методы обработки нажатий в класс VideoDisplay:

public function play():void {
    mediaPlayer.play();
}

public function pause():void {
    mediaPlayer.pause();
}

public function stop():void {
    mediaPlayer.stop();
}

6. Добавление кнопки "Full Screen"

Кнопка "Full Screen"

В файл разметки FlexOSMFPlayer.xml было добавлено описание кнопки входа/выхода в полноэкранный режим:

<s:ToggleButton
        id = "fullScreenButton"
        label = "Full Screen"
        click = "fullScreen(event)"/>

Добавлена функция fullScreen, которая срабатывает при клике на пнопку:

private function fullScreen(event:MouseEvent):void {
    if (event.target.selected) {
        videoDisplay.screenState(StageDisplayState.FULL_SCREEN);
        fullScreenButton.label = "Normal Screen";
    } else {
        videoDisplay.screenState(StageDisplayState.NORMAL);
        fullScreenButton.label = "Full Screen";
    }
}

В классе VideoDisplay добавлен метод screenState, отвечающий за обработку события изменения размера окна:

public function screenState(stageDisplayState:String):void {
    stage.displayState = stageDisplayState;
    container.width = stage.stageWidth;
    container.height = stage.stageHeight;
}

В функцию stop() файла FlexOSMFPlayer.xml добавлен вызов метода, позволяющего выходить из полноэкранного режима, если была нажата кнопка "Stop":

private function stop():void {
    ...
	videoDisplay.screenState(StageDisplayState.NORMAL);
}

Обнаружил интересную особенность, чтобы вход в полноэкранный режим заработал, нужно добавить в тег <s:Application> файла разметки: backgroundAlpha="0".

7. Добавление виджета громкости

Слайдер громкости

Виджет громкости состоит из двух лейблов и слайдера, сгруппированных по горизонтали. Вот как они описаны в файле FlexOSMFPlayer.xml:

<s:HGroup
        paddingTop="3">

    <s:Label
            textAlign="center"
            text = "Volume:"
            color = "white"
            backgroundColor = "0xa9a9a9"
            backgroundAlpha = "0.8"/>

    <s:HSlider
            id = "volumeSlider"
            minimum = "0"
            maximum = "100"
            value = "50"
            stepSize = "5"
            snapInterval = "5"
            liveDragging = "true"
            change = "volume(volumeSlider.value)"
            dataTipFormatFunction="volumeDataTipFormat"/>

    <s:Label
            id = "volumeLabel"
            textAlign="center"
            text = "{int(volumeSlider.value)}%"
            color = "white"
            backgroundColor = "0xa9a9a9"
            backgroundAlpha = "0.8"/>

</s:HGroup>

Пользователи привыкли видеть на экране значение громкости в процентах, в то время как сеттер volume класса MediaPlayer, фреймворка OSMF, принимает значение в виде вещественного числа, процесс конвертации описан в функции volume(volume:Number) файла разметки:

private function volume(volume:Number):void {
    videoDisplay.volume = volume / 100;
}

Описание сеттера volume класса VideoDisplay:

public function set volume(volume:Number):void {
    mediaPlayer.volume = volume
}

Теперь, при перемещении слайдера, меняется громкость и отображается текущее значение.

8. Добавление Timeline

Timeline

Timeline представляет собой ProgressBar:

<mx:ProgressBar
        y = "10"
        width="400"
        id = "progressBar"
        label = ""
        trackHeight = "18"
        minimum = "0"
        maximum = "{int(duration)}"
        mode = "manual"/>

В качестве его максимального значения используется переменная duration, которая пробрасывается из класса VideoDisplay в обработчике события TimeEvent.DURATION_CHANGE:

private function videoDurationChange(event:TimeEvent):void {
    dispatchEvent(event);
}

Для того, чтобы прогресс перемещался по мере воспроизведения видео, нужно отлавливать событие TimeEvent.CURRENT_TIME_CHANGE, которое было добавлено в метод createChildren() класса VideoDisplay:

override protected function createChildren():void {
    ...
    mediaPlayer = new MediaPlayer();
    ...
    mediaPlayer.addEventListener(TimeEvent.DURATION_CHANGE, videoDurationChange);
    mediaPlayer.addEventListener(TimeEvent.CURRENT_TIME_CHANGE, videoTimeChange);
	...
}

Реализация обработчика videoTimeChange пробрасывает событие в файл FlexOSMFPlayer.xml:

[Event(name="durationChange", type="org.osmf.events.TimeEvent")]
[Event(name="currentTimeChange", type="org.osmf.events.TimeEvent")]
public class VideoDisplay extends UIComponent {
    ...
    private function videoDurationChange(event:TimeEvent):void {
    	dispatchEvent(event);
	}

    private function videoTimeChange(event:TimeEvent):void {
        dispatchEvent(event);
    }
	...
}

Был изменен файл разметки, чтобы он мог принимать отправляемое ему событие

<flexosmf:VideoDisplay
        id = "videoDisplay"
        source= "{HTTP}"
        durationChange = "videoDurationChange(event)"
        currentTimeChange = "videoTimeChange(event)"/>

Функция videoTimeChange устанавливает текущее значение времени воспроизводимого видео в timeline:

    private function videoTimeChange(event:TimeEvent):void {
        currentTime = event.time;
        progressBar.setProgress(int(currentTime), int(duration));
    }

8.1 Перемещение по timeline

Перемещение осуществляется кликом кнопки мыши по timeline, для этого был добавлен обработчик события MouseEvent.CLICK для timeline:

progressBar.addEventListener(MouseEvent.CLICK, videoDisplay.onSeek);

Реализация обработчика onSeek класса VideoDisplay:

public function onSeek(event:MouseEvent):void {
    var seekTo:Number = mediaPlayer.duration * (event.target.mouseX / event.target.width);
    mediaPlayer.seek(seekTo);
}

8.2 Отображение возможного времени воспроизведения в tooltip при перемещении указателя мыши

Хотелось бы видеть время возможного воспроизведения перед щелчком мыши на timeline, для этого объекту класса ProgressBar был добавлен обработчик события MouseEvent.MOUSE_MOVE:

progressBar.addEventListener(MouseEvent.MOUSE_MOVE, showToolTipMouseMovie);

Дело оставалось за малым - показывать tooltip с возможным временем воспроизведения при перемещении указателя мыши. Первое, что я попробовал, добавил в методе showToolTipMouseMovie строку:

progressBar.toolTip = "MM:SS";

Всплывающая подсказка появлялась, но не перемещалась за курсором и в ней не изменялось время. Поэтому я воспользовался статическим методом createToolTip класса ToolTipManager в методе showToolTipMouseMovie. В результате, всплывающая подсказка перемещалась вслед за курсором мыши и меняла время, но была одна особенность - каждый раз создавалась новая подсказка и старые не исчезали. Для устранения этой проблемы я создал переменную private var mouseOverTimeToolTip:IToolTip; и воспользовался статическим методом destroyToolTip класса ToolTipManager, передав эту переменную в качестве параметра и ей же присвоил результат выполнение метода createToolTip. В итоге, получилось так:

private function showToolTipMouseMovie(event:MouseEvent):void {
    var mouseOverTime:Number = duration * (event.target.mouseX / event.target.width);

    if (mouseOverTimeToolTip != null)
        ToolTipManager.destroyToolTip(mouseOverTimeToolTip);

    mouseOverTimeToolTip = ToolTipManager.createToolTip(
            timeFormat(mouseOverTime), 
            event.target.mouseX, 
            progressBar.y + 15);
}

Последний аргумент progressBar.y + 15 метода createTookTip позволяет отображать всплывающую подсказку на одной горизонтальной линии, когда курсор движется в вертикальном направлении в пределах виджета.

Для привычного отображения времени была добавлена функция timeFormat:

private function timeFormat(time:int):String {
    var minutes:int = time / 60;
    var seconds:int = time % 60;
    var min:String = minutes < 10 ? "0" + minutes : "" + minutes;
    var sec:String = seconds < 10 ? "0" + seconds : "" + seconds;

    return min + ":" + sec;
}

Время возможного воспроизведения появлялось и следовало за курсором мыши на timeline, но не исчезало, когда курсор уходил за пределы виджета. В этот момент мне на помощь поспешил обработчик события MouseEvent.MOUSE_OUT:

progressBar.addEventListener(MouseEvent.MOUSE_OUT, hideToolTipMouseMovie);

В метод hideToolTipMouseMovie я добавил разрушитель всплывающей подсказки ToolTipManager.destroyToolTip(mouseOverTimeToolTip);:

private function hideToolTipMouseMovie(event:MouseEvent):void {
    if (mouseOverTimeToolTip != null)
        ToolTipManager.destroyToolTip(mouseOverTimeToolTip);

    mouseOverTimeToolTip = null;
} 

9. Добавление виджета с текущим и общем временем воспроизведения видео

Timeline

В качестве виджета используется текстовый лейбл, который описан в файле FlexOSMFPlayer.xml следующим образом:

<s:Label
        id = "playPositionLabel"
        paddingTop="5"
        textAlign="center"
        text = "Time: {timeFormat(currentTime) + ' / ' + timeFormat(duration)}"
        color = "white"
        backgroundColor = "0xa9a9a9"
        backgroundAlpha = "0.8"/>

Строка формируется из значений текущего времени воспроизведения и общей продолжительности видео, которые пробрасываются из класса VideoDisplay в переменные файла разметки FlexOSMFPlayer.xml:

VideoDisplay:

[Event(name="durationChange", type="org.osmf.events.TimeEvent")]
[Event(name="currentTimeChange", type="org.osmf.events.TimeEvent")]
...
public class VideoDisplay extends UIComponent {
...
    override protected function createChildren():void {
        ....
        mediaPlayer = new MediaPlayer();
        mediaPlayer.addEventListener(TimeEvent.DURATION_CHANGE, videoDurationChange);
        mediaPlayer.addEventListener(TimeEvent.CURRENT_TIME_CHANGE, videoTimeChange);
        ...
    }
    ...
    private function videoDurationChange(event:TimeEvent):void {
        dispatchEvent(event);
    }

    private function videoTimeChange(event:TimeEvent):void {
        dispatchEvent(event);
    }
    ...
}

FlexOSMFPlayer.xml:

[Bindable]
private var currentTime:Number;
[Bindable]
private var duration:Number;
...
private function videoDurationChange(event:TimeEvent):void {
    duration = event.time;
}

private function videoTimeChange(event:TimeEvent):void {
    currentTime = event.time;
    ...
}

Чтобы время отображалось в привычном формате, используется функция timeFormat, в которую передается значение времени в миллисекундах, а на выходе получаем строку вида MM:SS:

private function timeFormat(time:int):String {
    var minutes:int = time / 60;
    var seconds:int = time % 60;
    var min:String = minutes < 10 ? "0" + minutes : "" + minutes;
    var sec:String = seconds < 10 ? "0" + seconds : "" + seconds;

    return min + ":" + sec;
}

10. Добавление виджета отображения и изменения качества видео

Timeline

Виджет изменения качество видео состоит из выподающего списка и лейбла:

<s:DropDownList
        id = "changeStream"
        width="100"
        dataProvider="{streamDataProvider}"
        change="changeStreamChange(event)"/>

<s:Label
        id ="requestOrAutoInfoLabel"
        paddingTop="5"
        textAlign="center"
        text = ""
        color = "white"
        backgroundColor = "0xa9a9a9"
        backgroundAlpha = "0.8"/>

Выпадающий включает в себя объект с отображаемыми данными streamDataProvider и обработчик события изменения текущего элемента changeStreamChange(event). Лейбл отображает информацию в случае запроса к серверу на изменение качества видео, или информацию о том, включено ли автоматическое изменение качества.

10.1 DataProvider

В качестве провайдера данных я использовал ArrayList, элементами которого являются битрейты динамического потока воспроизводимого видео. Первым делом - нужно было проинициализировать список значениями в момент, когда доступны индексы потоков и их общее количество. И тут встал вопрос, а как подобрать этот момент? Я уже приступил к созданию собственного класса события, который бы мне позволил отловить этот момент, но нашел более простое и менее красивое решение, в обработчик videoTimeChange добавить код, который вполне справился с поставленной задачей:

private function videoTimeChange(event:TimeEvent):void {
    ...
    if (streamDataProvider.length < videoDisplay.numDynamicStreams) {
        for (var i:int = 0; i <= videoDisplay.maxAllowedDynamicStreamIndex; i++) {
            streamDataProvider.addItem(videoDisplay.getBitrateForDynamicStreamIndex(i));
        }
        streamDataProvider.addItem(AUTO);
    }
}

10.2 Обраотчик изменения качества видео

Из класса VideoDisplay в файл разметки FlexOSMFPlayer.xml было проброшено событие DynamicStreamEvent.SWITCHING_CHANGE следующим образом.

VideoDisplay:

[Event(name="switchingChange", type="org.osmf.events.DynamicStreamEvent")]
public class VideoDisplay extends UIComponent {
    ...
    override protected function createChildren():void {
        ...
        mediaPlayer = new MediaPlayer();
        ...
        mediaPlayer.addEventListener(DynamicStreamEvent.SWITCHING_CHANGE, onSwitchingChange);
        ...
    }
    ...
    private function onSwitchingChange(event:DynamicStreamEvent):void {
        dispatchEvent(event);
    }
   ...
}

FlexOSMFPlayer.xml:

private function switchingChange(event:DynamicStreamEvent):void {
    var requestMsg:String = "";
    var showCurrentIndex:Boolean = false;

    if (event.switching) {
        requestMsg += "REQUESTING";
    } else {
        if (videoDisplay.autoDynamicStreamSwitch) {
            requestMsg += AUTO;
        }
        showCurrentIndex = true;
    }

    requestOrAutoInfoLabel.text = requestMsg;

    if (showCurrentIndex) {
        changeStream.setSelectedIndex(videoDisplay.currentDynamicStreamIndex);
    }
}

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

Ниже описан метод, отвечающий за ручное изменение качества видео, путем выбора элемента из выпадающего списка:

private function changeStreamChange(event:IndexChangeEvent):void {
    var streamIndex:int = event.newIndex;
    if (streamIndex <= videoDisplay.maxAllowedDynamicStreamIndex) {
        videoDisplay.autoDynamicStreamSwitch = false;
        videoDisplay.switchDynamicStreamIndex(streamIndex)
    } else {
        videoDisplay.autoDynamicStreamSwitch = true;
    }
}

В классе VideoDisplay добавлены методы, отвечающие за переключение качества видео:

public function set autoDynamicStreamSwitch(b : Boolean):void {
    mediaPlayer.autoDynamicStreamSwitch = b;
}

public function get autoDynamicStreamSwitch():Boolean {
    return mediaPlayer.autoDynamicStreamSwitch;
}

public function get numDynamicStreams():int {
    return mediaPlayer.numDynamicStreams;
}

public function get currentDynamicStreamIndex():int {
    return mediaPlayer.currentDynamicStreamIndex;
}

public function get maxAllowedDynamicStreamIndex():int {
    return mediaPlayer.maxAllowedDynamicStreamIndex;
}

public function getBitrateForDynamicStreamIndex(dynamicStreamIntex:int):Number {
    var bitrate:Number = mediaPlayer.getBitrateForDynamicStreamIndex(dynamicStreamIntex);
    return bitrate;
}

public function switchDynamicStreamIndex(streamIndex:int):void {
    mediaPlayer.switchDynamicStreamIndex(streamIndex);
}
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.