sobota, 1 września 2012

WinJS na żywo - odc. 3 (m.in pobieranie plików w tle, komponent WinRT, wyświetlanie miniatur plików w ListView, odtwarzanie video)

Przepisywanie z XAML i C# na HTML5 i WinJS nabrało tempa. Obecnie mogę pobierać pliki, przeglądać je i odtwarzać. Na tej drodze było parę niespodzianek, parę ciekawostek i na te tematy będzie dzisiejszy post.

Pewne zmiany nastąpiły przy przepisywaniu logiki wyciągania adresu ze strony YouTube z C# na JS (tak wiem, mogłem tego nie przepisywać, zrobić sobie komponent WinRT w C#, ale chciałem zobaczyć jak wyjdzie to całościowo w Java Script). O ile trzeba pamiętać, że niektóre tak samo nazywające się metody przy podaniu takich samych parametrów zadziałają inaczej (np. substring), o tyle kwestia unescapingu podanego napisu z linkiem okazała się trudniejsza. W WinJS mamy do tego metodę Windows.Foundation.Uri.unescapeComponent(toUnescape), w .NET mamy podobną metodę Uri.UnescapeDataString(stringToUnescape). Fragment strony HTML był z powodzeniem przetwarzany przez metodę w .NET, ale w Java Script już nie (zdaje się, że nie są to do końca odpowiedniki - JS ma jedną metodę do escape-owania spodziewającą się Uri, .NET ma dwie metody - jedną dla Uri i jedną dla stringa ze słowem DataString). Może mogłem przebudować algorytm tak by lepiej pasował do dostępnej funkcji w JavaScript, ale na tym etapie nie uznałem tego za celowe, więc … użyłem metody z C#. Jak? Opakowałem ją w komponent WinRT:

public sealed class EncodingUtils
    {
        public string UnescapeDataString(string stringToUnescape)
        {
            return Uri.UnescapeDataString(stringToUnescape);
        }
    }

i wykorzystuję w kodzie Java Script:

(function () {
    "use strict";   

var utils = new MyTubeUtils.EncodingUtils();

function getFormatUrl(buffer, format) {
      ….

     var str3 = utils.unescapeDataString(subStr);

      ….

}

})();

Przejdźmy teraz do pobierania plików w tle. Całkiem dobra implementacja zawarta jest w referencyjnym Background Transfer sample, przy czym tam zlecanie i wyświetlanie pobrań odbywa się w ramach jednej strony. U mnie zlecanie pobierania plików odbywa się z poziomu innej strony (Add New Videos) niż monitorowanie pobrań (Start), więc tę czynność musiałem sobie wydzielić:

function downloadFile(fileName, url) {

        var uri = new Windows.Foundation.Uri(url);

        Windows.Storage.ApplicationData.current.temporaryFolder.createFileAsync(fileName, Windows.Storage.CreationCollisionOption.replaceExisting).done(function (newFile) {
            var downloader = new Windows.Networking.BackgroundTransfer.BackgroundDownloader();
            var download = downloader.createDownload(uri, newFile);
            download.startAsync();
        });       
    }

Po otwarciu strony startowej zawsze sprawdzam, czy nie ma aktywnych pobrań plików i ustawiam w prawym górnym rogu odpowiedni status. Gdy plik zostanie z sukcesem pobrany przenoszę go z temporaryFolder do localFolder. Pojawia się nam teraz kolejne zagadnienie jak prezentować w ListView pobrane pliki video z miniaturami.

jstube_7

W C# używałem do tego klasy FileInformationFactory, która dla zdefiniowanego file query zwracała wirtualizowany wektor plików, który można było zbindować do ListView. Dzięki temu mieliśmy automatyczne odświeżanie listy, gdy nowy plik został pobrany (zapisany w localFolder). Jak to wygląda w WinJS? Okazuje się, że mamy podobne możliwości, ale nieco inne API.

Zacznijmy od zdefiniowania file query, którego definiowanie przypomina definiowanie w C#. W moim przypadku przedstawia się to następująco:

var folder = Windows.Storage.ApplicationData.current.localFolder;

var queryOptions = new Windows.Storage.Search.QueryOptions(Windows.Storage.Search.CommonFileQuery.defaultQuery, [".mp4"]);
queryOptions.folderDepth = Windows.Storage.Search.FolderDepth.deep;
queryOptions.indexerOption = Windows.Storage.Search.IndexerOption.useIndexerWhenAvailable;

var fileQuery = folder.createFileQueryWithOptions(queryOptions);

Musiałem dodać tutaj filtr na pliki o rozszerzeniu .mp4 (w takim formacie zapisuję na razie wszystkie pobierane pliki), ponieważ zauważyłem że aplikacja pisana w HTML5 tworzy sobie plik sesji w lokalnym folderze i … na początku trafił mi do ListView.

I teraz rzecz najciekawsza. W WinJS mamy specjalny storage typu StorageDataSource, który należy zbindować do ListView:

var dataSourceOptions = {              
                mode: Windows.Storage.FileProperties.ThumbnailMode.picturesView,
                requestedThumbnailSize: 283,
                thumbnailOptions: Windows.Storage.FileProperties.ThumbnailOptions.useCurrentScale
            };

var dataSource = new WinJS.UI.StorageDataSource(fileQuery, dataSourceOptions);

listView.itemDataSource = dataSource;

Dzięki temu mamy jak w C# automatyczne odświeżanie ListView, jeśli zapiszemy do localStorage nowy plik .mp4. Ze StorageDataSource wiąże się też parę faktów. Nie wspiera widoków grupowych oraz wymaga określonego rodzaju bindingów. Przy uruchamianiu aplikacji z dotychczas używanym szablonem dla elementu listy, otrzymałem wyjątek Cannot define property '_getObservable': object is not extensible. Trzeba zmienić domyślny rodzaj bindingu na onetime. Przykładowo, aby na szablonie elementu wyświetlić tytuł nagrania, robimy to tak:

<h4 class="item-title" data-win-bind="textContent: name WinJS.Binding.oneTime"></h4>

Jest też kwestia wyświetlenia obrazka na elemencie. W C# miałem do tego konwerter, który tworzył mi BitmapImage ze strumienia. W WinJS wyświetlenia obrazka również nie dostaniemy za darmo, trzeba napisać sobie własną funkcję do bindingu: 

(function () {
    "use strict";

function bindThumbnail(source, sourceProperty, destination, destinationProperty) {

    …
}

    WinJS.Utilities.markSupportedForProcessing(bindThumbnail);

    WinJS.Namespace.define("Start", {       
        bindThumbnail: bindThumbnail
    });
})();

Metoda WinJS.Utilities.markSupportedForProcessing służy do rejestracji naszej metody do deklaratywnego użycia (cześć bardziej standardowych metod np. dla strony, dla konwersji ma to zapewnione automatycznie)

Polecam tutaj skorzystać z implementacji bindThumbnail z referencyjnego StorageDataSource and GetVirtualizedFilesVector sample. Jak zobaczymy, oprócz konwersji obiektu pliku na URL i przypisania do do image.src autorzy wzbogacili ją o animacje w wybranych przypadkach. Przy okazji mała dygresja - pisanie bindingów własnego rodzaju za pomocą funkcji kojarzy mi się z Knockoutem.

Ostatecznie szablon elementu dla ListView wygląda następująco:

   <div class="itemtemplate" data-win-control="WinJS.Binding.Template">
        <div class="item">
            <img class="item-image" src="#" data-win-bind="src: thumbnail Start.bindThumbnail" style="opacity: 0;"/>
            <div class="item-overlay">
                <h4 class="item-title" data-win-bind="textContent: name WinJS.Binding.oneTime"></h4>               
            </div>
        </div>
    </div>

Teraz ostatnie na dziś główne zagadnienie - odtwarzanie plików video.

jstube_8

Aby po dotknięciu elementu ListView przejść na stronę, na której wyświetlimy wideo przydatny będzie zdefiniowanie funkcji _itemInvoked w funkcji definiującej stronę:

ui.Pages.define("/pages/start/start.html", {

….,

_itemInvoked: function (args) {
            args.detail.itemPromise.then(function (invokedItem) {
                nav.navigate("/pages/videoDetails/videoDetails.html", { video: invokedItem.data });
            }, false);                    
        }

Możemy teraz na stronie z wideo zamienić otrzymane dane na URL, który przekażemy elementowi <video> z HTML5.

WinJS.UI.Pages.define("/pages/videoDetails/videoDetails.html", {
       ready: function (element, options) {
            if (options && options.video) {
                var fileItem = options.video;
                var video = document.getElementById("player");
                video.src = URL.createObjectURL(fileItem);
                video.play();
            }           
        }
    });

Należy tu dodać, że dostajemy za darmo ładnie ostylowane UI z paskiem postępu, przyciskami i regulacją głośności (btw w przeglądarkach kontrolka <video> też oferuje podobne możliwości i jest otrzymuje predefiniowane UI, w aplikacji Windows 8 dostajemy styl Modern UI/Metro). Aby otrzymać wygląd kontrolki z przyciskami ze screenshota powyżej wystarczy wewnątrz strony użyć:

<video id="player" controls>                        
</video>

Oczywiście trochę trzeba było przestylować domyślny szablon strony, zmienić trochę CSS-ów, by kontrolka <video> zajmowała cały obszar strony i aby znajdowała się pod przyciskiem Back. Zastosowałem tutaj z-index by sekcja z nagłówkiem pojawiała się pod contentem strony z wideo. Aczkolwiek z nowym Blendem była to czysta przyjemność…

Jeszcze małe porównanie z XAML i C#. W wersji końcowej Windows 8 została wyrzucona kontrolka MediaPlayer, mamy do dyspozycji w jedynie MediaElement, który nie ma predefiniowanego UI do kontroli odtwarzania… W tym przypadku lepiej wygląda to w HTML5, ale piszący w C# mogą skorzystać z Player Framework for Windows 8, który oferuje gotową kontrolkę odtwarzacza oraz z przykładów.

Ciąg dalszy nastąpi.

Brak komentarzy: