Создание кроссплатформенного плеера для SoundCloud® с Fuse

January 2017

Разработка: Безопасность: Кроссплатформенный плеер для SoundCloud® с Fuse

Мы по­сто­ян­но по­лу­ча­ем за­про­сы от на­ших поль­зо­ва­те­лей, ко­то­рые хо­тят уви­деть, как вы­гля­дят “ре­аль­ные про­грам­мы”, сде­лан­ные с Fuse. Наш учеб­ник спе­ци­аль­но пред­на­зна­чен для быст­ро­го на­ча­ла ра­бо­ты с Fuse, но при этом он по­ка не со­дер­жит опи­са­ний слож­ных за­дач.

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

При­ло­же­ние бу­дет ра­бо­тать с ре­аль­ным бе­кен­дом, оно бу­дет крос­сплат­фор­мен­ным (ра­бо­тать в Android и iOS) и бу­дет ис­поль­зо­вать несколь­ко ин­те­рес­ных на­тив­ных ин­те­гра­ций на обе­их плат­фор­мах.

Как мы ви­дим, SoundCloud® име­ет всё необ­хо­ди­мое:

  • Они предо­став­ля­ют REST API (и он бес­пла­тен)
  • Есть мно­го кон­тен­та для ра­бо­ты (кар­тин­ки и му­зы­ка)
  • На­ше­му при­ло­же­нию по­на­до­бят­ся на­тив­ные ком­по­нен­ты (кон­тро­лы для управ­ле­ния му­зы­кой и т.п.)

Пе­ред тем, как на­чать…

Вы мо­же­те не уста­нав­ли­вать Fuse и не чи­тать код на Github, что­бы уви­деть го­то­вый ре­зуль­тат. При­ло­же­ние FuseCloud мож­но ска­чать в Apple App Store и Google Play.

Вни­ма­ние: при­ло­же­ние FuseCloud это неофи­ци­аль­ный пле­ер для SoundCloud, и ни­ка­ким об­ра­зом не свя­за­но с SoundCloud. Оно про­сто ис­поль­зу­ет SoundCloud API.

Ре­а­ли­за­ция

Есть три глав­ных под­за­да­чи: поль­зо­ва­тель­ский ин­тер­фейс, на­пи­сан­ный на UX и JavaScript; обёрт­ка во­круг SoundCloud REST API; на­тив­ный му­зы­каль­ный пле­ер.

На­ви­га­ция

Я ис­поль­зо­вал ком­по­нен­ты Router и Navigator для по­стро­е­ния боль­шей ча­сти на­ви­га­ции, с един­ствен­ным ис­клю­че­ни­ем для PageControl в глав­ном пред­став­ле­нии, в ко­то­ром вы мо­же­те пе­ре­клю­чать­ся меж­ду тре­мя та­ба­ми (лен­та но­во­стей, по­иск, из­бран­ное). По­сле со­зда­ния каж­дой стра­ни­цы при­ло­же­ния от­дель­ным ком­по­нен­том, на­ви­га­ция бу­дет при­мер­но та­кой:

// fusecloudnavigationstructure.ux
<Navigator DefaultTemplate="main">

    <FuseCloud.MainPage ux:Name="main">
        <PageControl ux:Name="pageControl" Active="searchPage">
            <Page ux:Name="newsFeedPage" />
            <Page ux:Name="searchPage" />
            <Page ux:Name="favoritesPage" />
        </PageControl>
    </FuseCloud.MainPage>

    <FuseCloud.CommentsPage ux:Template="comments" router="router" />

    <FuseCloud.TrackDetailsPage ux:Name="track" router="router"/>

</Navigator>
Бес­ко­неч­ная про­крут­ка

Бла­го­да­ря но­вым воз­мож­но­стям Fuse, я лег­ко со­здал плав­ную бес­ко­неч­ную про­крут­ку для отоб­ра­же­ния всех ком­мен­та­ри­ев к каж­дой ком­по­зи­ции. Встав­ляя от­дель­ные ком­мен­та­рии в блок Deferred, я за­щи­щён от глю­ков при ав­то­ма­ти­че­ской под­груз­ке но­вых эле­мен­тов. Ни­же вы мо­же­те уви­деть при­мер UX-ко­да для со­зда­ния та­кой про­крут­ки:

// fusecloudendlessscroller.ux
<ScrollView ClipToBounds="False">
    <StackPanel>
        <Each Items="{comments}">
            <Deferred>
                <FuseCloud.DividerLine Alignment="Top"/>
                <FuseCloud.Comment ux:Name="comment" ThumbnailUrl="{avatar_url}" Username="{username}" Body="{body}" />
            </Deferred>
        </Each>
    </StackPanel>
    <Scrolled To="End" Within="100">
        <Callback Handler="{showMoreComments}" />
    </Scrolled>
</ScrollView>

Про­крут­ка на 100 то­чек от ниж­не­го края при­ло­же­ния вы­зы­ва­ет JavaScript-функ­цию, ко­то­рая под­гру­жа­ет сле­ду­ю­щие ком­мен­та­рии:

// fusecloudendlessscroller.js
function showMoreComments() {
    if (nCommentsShowing < allComments.length) {
        nCommentsShowing += nCommentsPerPage;
        while (comments.length < nCommentsShowing && comments.length < allComments.length - 1) {
            comments.add(allComments.getAt(comments.length));
        }
    }
}
За­мут­не­ние фо­на

Экран про­иг­ры­ва­те­ля по­ка­зы­ва­ет изоб­ра­же­ние аль­бо­ма те­ку­щей до­рож­ки по­се­ре­дине стра­ни­цы, при этом за­пол­няя весь фон за­тем­нён­ной ко­пи­ей это­го же изоб­ра­же­ния. В Fuse та­кое де­ла­ет­ся од­ной строч­кой ко­да: , но за­мут­не­ние боль­ших эле­мен­тов мо­жет пло­хо от­ра­зить­ся на про­из­во­ди­тель­но­сти. По­это­му я ис­поль­зую клас­си­че­ский трюк с про­грамм­ной GPU-об­ра­бот­кой для улуч­ше­ния ско­ро­сти:

// fusecloudscaledblur.ux
<FuseCloud.AlbumArt Width="20%" Height="20%">
    <Blur Radius="2"/>
    <Scaling Factor="5" />
</FuseCloud.AlbumArt>

Здесь я умень­шаю раз­мер изоб­ра­же­ния до 20% от ори­ги­наль­но­го и за­мут­няю умень­шен­ное изоб­ра­же­ние, за­тем уве­ли­чи­ваю по­лу­чив­ше­е­ся изоб­ра­же­ние до нор­маль­но­го раз­ме­ра.

Разработка: Кроссплатформенный плеер для SoundCloud® с Fuse

Ра­бо­та­ем с SoundCloud API

Во­об­ще это неслож­ная за­да­ча и в ней нет ни­ка­ких спе­ци­фич­ных для Fuse тех­ник. Я струк­ту­ри­ро­вал обёрт­ку, по­это­му каж­дый за­прос воз­вра­ща­ет promise. Мо­дель при­ло­же­ния бы­ла ис­поль­зо­ва­на как ин­тер­фейс к API че­рез на­бор функ­ций-гет­те­ров, воз­вра­щав­ших promise-ы в Observable. Код ни­же на­гляд­но ил­лю­стри­ру­ет этот под­ход:

Функ­ция, ис­поль­зу­е­мая для по­лу­че­ния ста­ту­са лай­ка для тре­ка, воз­вра­ща­ет promise:

// fusecloudislikingtrackfetch.js
function isLikingTrack(trackId) {
    return Auth.getAccessToken()
        .then(function(token) {
            return FuseCloudGet("me/favorites/" + trackId, {}, token);
        });
}

Мо­дель пре­вра­ща­ет этот promise в observable, ис­поль­зуя удоб­ную функ­цию (DelayedObservable):

// fusecloudislikingtrackobservable.js
function GetIsLikingTrack(trackId) {
    return DelayedObservable(function(obs) {
        FuseCloud.isLikingTrack(trackId)
            .then(function(result) {
                obs.add(result);
            });
    });
}

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

Функ­ция DelayedObservable ра­бо­та­ет как мост меж­ду API, ос­но­ван­ной на promise-ах, и API, ос­но­ван­ной на Observable:

// fuseclouddelayedobservable.js
function DelayedObservable(getter) {
    var ret = Observable();
    getter(ret);
    return ret;
}

Эта функ­ция от­ве­ча­ет за об­нов­ле­ние Observable при за­груз­ке дан­ных.

OAuth 2.0

SoundCloud API поз­во­ля­ет ав­то­ри­зо­вать­ся с по­мо­щью про­то­ко­ла OAuth 2.0. Ис­поль­зуя мо­дуль InterApp, я лег­ко пе­ре­ки­ды­ваю поль­зо­ва­те­ля на ав­то­ри­за­цию с по­мо­щью на­тив­но­го бра­у­зе­ра:

// fusecloudlaunchuri.js
var uri = "https://soundcloud.com/connect?client_id=" + clientId
        + "&display=popup"
        + "&response_type=code"
        + "&redirect_uri=fuse-soundcloud://fuse";
InterApp.launchUri(uri);

В URL вы­ше пе­ре­да­ёт­ся URI об­рат­но­го вы­зо­ва, ко­то­рый SoundCloud ис­поль­зу­ет для воз­вра­та то­ке­на. Fuse поз­во­ля­ет за­ре­ги­стри­ро­вать свою URI-схе­му в фай­ле про­ек­та:

// fusecloudcustomurischeme.json
"Mobile":{
    "UriScheme": "fuse-soundcloud"
}

Та­ким об­ра­зом, SoundCloud API ав­то­ма­ти­че­ски вер­нёт­ся в на­шу про­грам­му, как толь­ко то­кен до­сту­па бу­дет го­тов.

Со­зда­ние крос­сплат­фор­мен­но­го аудио­пле­е­ра

Ре­а­ли­за­ция обёрт­ки для на­тив­ных пле­е­ров бы­ла наи­бо­лее ин­те­рес­ной ча­стью про­цес­са. Ча­стич­но из-за то­го, что API для это­го раз­лич­ны у Android и iOS, но та­к­же по при­чине мо­но­лит­ной при­ро­ды ме­диа-пле­е­ров. Я на­чал с ми­ни­маль­но­го на­бо­ра тре­бо­ва­ний.

Наш StreamingPlayer дол­жен:

  • Транс­ли­ро­вать аудио по URL
  • Про­дол­жать иг­рать, ко­гда при­ло­же­ние ухо­дит в фон
  • Поз­во­лять пе­ре­клю­чать­ся меж­ду тре­ка­ми, по­ка при­ло­же­ние на­хо­дит­ся в фоне (ис­поль­зуя на­тив­ные кон­тро­лы на экране бло­ки­ров­ки)
  • Отоб­ра­жать об­лож­ку аль­бо­ма на экране бло­ки­ров­ки
    Разработка: Кроссплатформенный плеер для SoundCloud® с Fuse

На бу­ма­ге это не ка­жет­ся слиш­ком слож­ной за­да­чей, но на де­ле она ока­за­лась на­сто­я­щим вы­зо­вом.

Пре­жде все­го, под­клю­че­ние к на­тив­но­му аудио­пле­е­ру для про­иг­ры­ва­ния URL бы­ло су­пер­про­стым. API у Android MediaPlayer-а и AVPlayer у iOS пред­ла­га­ют это пря­мо из ко­роб­ки. Мо­ей на­чаль­ной за­дум­кой бы­ло ис­поль­зо­вать ми­ни­маль­ную обёрт­ку во­круг обо­их этих API и про­сто де­лать осталь­ную ра­бо­ту (ти­па управ­ле­ния плей­ли­ста­ми и со­сто­я­ни­ем) в JavaScript. Но огра­ни­че­ние на фо­но­вое вы­пол­не­ние JS на этих плат­фор­мах по­ста­ви­ло крест на этом (од­но из на­ших тре­бо­ва­ний — воз­мож­ность ис­поль­зо­вать кон­тро­лы на экране бло­ки­ров­ки).

Это озна­ча­ло, что я дол­жен ре­а­ли­зо­вать ра­бо­ту с плей­ли­ста­ми в на­тив­ном ко­де, при этом учи­ты­вая осо­бен­но­сти Android и iOS. К сча­стью, всё ока­за­лось го­раз­до про­ще, так как воз­мож­но­сти внеш­не­го ко­да Fuse поз­во­ля­ют вам лег­ко ин­те­гри­ро­вать код на Java и Objective-C в про­ек­ты Fuse. Это очень удоб­но!

Дру­гой ин­те­рес­ной за­да­чей бы­ло по­лу­че­ние те­ку­ще­го со­сто­я­ния пле­е­ра для обо­их ком­по­нен­тов, MediaPlayer и AVPlayer. Оба этих API име­ют раз­ные мо­де­ли со­сто­я­ния и раз­ные пу­ти управ­ле­ния ими, но я на­шёл уни­вер­саль­ный спо­соб.

И, на­ко­нец, ра­бо­та с экра­ном бло­ки­ров­ки. В iOS это крайне про­сто; до­ста­точ­но за­ре­ги­стри­ро­вать несколь­ко си­стем­ных вы­зо­вов. В Android же это не та­кая про­стая за­да­ча. В API, на­чи­ная с уров­ня 21, Android мо­жет по­лу­чать ме­диа-но­ти­фи­ка­ции, ко­то­рые за­ме­ща­ют обыч­ные кон­тро­лы на экране бло­ки­ров­ки. Но нуж­но ко­пать в сто­ро­ну си­сте­мы intent-ов для на­строй­ки ком­му­ни­ка­ции меж­ду но­ти­фи­ка­ци­я­ми и фо­но­вой служ­бой.

Воз­мож­но­сти

В при­ло­же­ние FuseCloud встро­е­ны очень боль­шие воз­мож­но­сти и ме­ха­низ­мы, и так как я люб­лю пе­ре­чис­ле­ния, вот неболь­шой спи­сок фич, за­ло­жен­ных в этой про­грам­ме (и ис­ход­ном ко­де):

  • Аутен­ти­фи­ка­ция в SoundCloud® по про­то­ко­лу OAuth 2.0
  • Ис­поль­зо­ва­ние па­ке­та InterApp для за­пус­ка url во внеш­нем бра­у­зе­ре и пе­ре­да­ча от­кли­ка по URI
  • Ав­то­ма­ти­че­ское об­нов­ле­ние некор­рект­ных то­ке­нов
  • По­лу­че­ние дан­ных по REST API
  • Лен­та но­во­стей, по­иск тре­ков, из­бран­ное
  • Воз­мож­ность по­ста­вить лайк и диз­лайк тре­ку
  • Об­лож­ки тре­ков
  • Отоб­ра­же­ние ком­мен­та­ри­ев к тре­ку
  • Раз­ме­ще­ние ком­мен­та­ри­ев
  • Ста­ти­сти­ка поль­зо­ва­те­ля
  • Сма­хи­ва­ние вле­во/впра­во для пе­ре­клю­че­ния до­рож­ки
  • По­тя­ги­ва­ние экра­на для об­нов­ле­ния
  • Бес­ко­неч­ный спи­сок про­крут­ки
  • Сма­хи­ва­ние для по­ка­за дей­ствий с эле­мен­том (диз­лайк в из­бран­ном)
  • Со­хра­не­ние со­сто­я­ния UI с ис­поль­зо­ва­ни­ем Storage API (при­вет­ствен­ная ин­фор­ма­ция по­ка­зы­ва­ет­ся толь­ко один раз при на­ча­ле ра­бо­ты с про­грам­мой)®
  • HTTP Audio StreamingPlayer для iOS и Android
  • Транс­ля­ция му­зы­ки из SoundCloud®
  • На­стра­и­ва­е­мая па­нель пе­ре­мот­ки
  • Фо­но­вое про­иг­ры­ва­ние
  • Кон­тро­лы на экране бло­ки­ров­ки в iOS и Android
  • iOS: сле­ду­ю­щий, преды­ду­щий, иг­рать/па­у­за, пе­ре­мот­ка на экране бло­ки­ров­ки
  • Об­лож­ка аль­бо­ма на экране бло­ки­ров­ки
  • Но­ти­фи­ка­ции в Android: сле­ду­ю­щий, преды­ду­щий, иг­рать/па­у­за
  • По­каз об­лож­ки аль­бо­ма в но­ти­фи­ка­ции и в фоне
  • Плей­ли­сты
  • Ав­то­про­иг­ры­ва­ние сле­ду­ю­ще­го при окон­ча­нии тре­ка

Вы­во­ды и за­груз­ки

Бы­ло ре­аль­но класс­но ра­бо­тать над этим про­ек­том. Я необъ­ек­ти­вен, но Fuse ре­аль­но впе­чат­ли­ла ме­ня  — в ко­то­рый раз.

И ме­ха­низ­мы внеш­не­го ко­да Fuse ока­за­лись дей­стви­тель­но хо­ро­шим спо­со­бом со­зда­ния на­тив­ных ком­по­нен­тов. Они поз­во­ля­ют ис­поль­зо­вать до­ку­мен­та­цию к API каж­дой плат­фор­мы на сво­ём язы­ке, где это воз­мож­но, и без обёр­ток на JavaScript.

Вы мо­же­те ска­чать при­ло­же­ние FuseCloud для Android и iOS в Apple App Store, в Google Play, и ис­ход­ный код на Github.

Вни­ма­ние: ещё раз о со­зда­нии “ре­аль­ной” про­грам­мы (с на­тив­ны­ми ком­по­нен­та­ми и ин­те­гра­ци­я­ми с бе­кен­дом) — вы по­чти на­вер­ня­ка столк­не­тесь с неко­то­ры­ми труд­но­стя­ми. Мы по­сто­ян­но улуч­ша­ем на­шу до­ку­мен­та­цию, но ес­ли всё-та­ки встре­ти­те та­кой слу­чай, дай­те знать об этом нам и со­об­ще­ству, и мы с ра­до­стью вам по­мо­жем :)

Узнать боль­ше о Fuse мож­но по­смот­рев по­сто­ян­но рас­ту­щий спи­сок при­ме­ров (с ис­ход­ным ко­дом, ко­неч­но), всту­пай­те в на­ше со­об­ще­ство (у нас есть класс­ный фо­рум и груп­па в Slack) или под­пи­сы­вай­тесь на нас в Twitter или Facebook.

Ав­тор ори­ги­на­ла Kristian Hasselknippe, Software Engineer at Fuse

comments powered by Disqus