Spis treści

  1. Serwis "muzyczny" YouTube ... offline?
  2. Ale, ale!
  3. FN_1: Przeniesienie youtube-dl do chmury
  4. FN_2: Funkcja-przelotka w C#
  5. Odtwarzacz muzyki w HTML5
  6. Wystawianie odtwarzacza HTML na świat
  7. Miłego słuchania w trybie offline!

Serwis "muzyczny" YouTube ... offline?

Na YouTube znajdują się praktycznie wszystkie teledyski utworów muzycznych. Żeby jednak tylko posłuchać fajnej muzyki, korzystając np. ze smartfona, ekran musi się świecić. Wygaszenie ekranu spowoduje zatrzymanie odtwarzania przez aplikację YouTube.

Z pomocą przychodzi aplikacja youtube-dl. W aplikacji można wskazać, że interesuje nas tylko ścieżka muzyczna (flaga -f numer_formatu). Potem trzeba przerzucić plik na telefon, itd., itd.

Ale, ale!

FN_1: Przeniesienie youtube-dl do chmury

Youtube-dl został dostosowany tak (na początku tak nie było) aby można było praktycznie korzystać z niego jak z biblioteki oferującej pewien zestaw API. Dlatego wystarczy rozpisać klasę dziedziczącą z YoutubeDL i trochę kodu dla Azure Functions.

Oczywiście nie jest tak różowo bo funkcje Azure napisane w Pythonie potrafią zwracać tylko ciągi znaków i to jeszcze w cudzysłowach! Dlatego rozwiązaniem na szybko jest zwykła konwersja muzyki w formacie WEBM na ciąg znaków w formacie szesnastkowym. Czyli wynikiem działania funkcji będzie ciąg wyglądający mniej więcej tak jak poniżej (tak, cudzysłowy też będą).

"FAC3..."

Poniżej znajduje się implementacja funkcji pobierającej muzykę w formacie WEBM.

# plik ./run.py
#
import os
import io
import json
import urllib2

from youtube_dl import YoutubeDL

class YDL(YoutubeDL):
    def __init__(self, *args, **kwargs):
        super(YDL, self).__init__(*args, **kwargs)

        self.jsonMsg = None

    def to_screen(self, msg, skip_eol=False):
        pass

    def report_warning(self, msg):
        pass

    def to_stdout(self, msg, skip_eol=False, check_quiet=False):
        # youtube-dl podczas działania rzuca różnymi śmieciami
        # nas interesuje tylko JSON wyzwolony flagą 'dump_single_json'
        if msg.startswith('{"'):
            self.jsonMsg = msg


def getRawUrl(urly):
    ydl = YDL({
        'simulate': True,
        'quiet': True,
        'dump_single_json': True
        })

    all_urls = [urly]

    # uruchomienie youtube-dl
    ydl.download(all_urls)

    # jeśli pojawił się JSON to wybieramy pierwszą ścieżkę WEBM
    if ydl.jsonMsg != None:
        cfg = json.loads ( ydl.jsonMsg )
        for fmt in cfg.get('formats', None):
            if 'webm' != fmt.get('ext', None):
                continue
            urlm = fmt.get('url', None)
            return urlm
            break

# standardowy kod z przykładu nowej funkcji Azure w Python...
postreqdata = json.loads(open(os.environ['req']).read())

rurl = postreqdata['rurl']
print ("getting url nfo", rurl)
wurl = getRawUrl(rurl)
os_res = os.environ['res']
print('os_res', type(os_res), os_res)
response = open(os_res, 'wb')

# problem niestety jest taki, że funkcja Azure zwraca zawsze
# tylko ciąg znaków otoczony cudzysłowami niezależnie od tego
# czy strumień 'response' jest wskazany jako binarny ('wb')

ctx = urllib2.urlopen(wurl)

# konwersja binarnej zawartości na ciąg szesnastkowy...
hexed_data = ''.join(format(x,'02x') for x in bytearray(ctx.read()))

response.write(hexed_data)
response.close()

Należy uważać na strukturę katalogów, z poszczególnymi elementami funkcji - tj. źródłami youtube-dl. Musi ona wyglądać tak:

(root)
|   run.py
|   function.json
|
+--- youtube_dl         // cały katalog z GIT
    |   YoutubeDL.py
    |   ...             // pozostałe pliki
    |
    +--- downloader
    +--- extractor
    +--- postprocessor

Istnieją co najmniej dwa sposoby jak wrzucić zawartość katalogu "youtube_dl".

Funkcję warto pozostawić zabezpieczoną kluczem API aby nikt niepowołany nie mógł z niej korzystać - Authorization level: Function w drzewku Integrate.

FN_2: Funkcja-przelotka w C#

W związku z problemem funkcji w Python (nie można zwrócić ciągu binarnego) należy stworzyć funkcję przelotkę, która zamieni szesnastkowy ciąg znaków na ciąg bajtów.

Funkcja oferuje API w postaci wywołania poniższego wywołania. Po wejściu na ten link przeglądarka internetowa Chrome po kilku-kilkunastu sekundach (czas zależny od przetwarzania przez youtube-dl) zacznie odtwarzać muzykę.

https://....azurewebsites.net/api/fn_name?vid={URL_FILMU}

Nie należy zapomnieć o ustawieniu poniższych elementów w kodzie:

// plik ./run.csx
//
using System.Net;
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
    log.Info("C# HTTP trigger function processed a request.");

    string vid = req.GetQueryNameValuePairs()
        .FirstOrDefault(q => string.Compare(q.Key, "vid", true) == 0)
        .Value;

    // Get request body
    dynamic data = await req.Content.ReadAsAsync<object>();

    // Set name to query string or body data
    vid = vid ?? data?.vid;

    if (string.IsNullOrEmpty(vid))
        return req.CreateResponse(HttpStatusCode.BadRequest, "No url!");

    log.Info("vid=" + vid);
    return await hexy2webmAsync(vid);
}

public static byte[] StringToByteArray(String hex)
{
    int NumberChars = hex.Length;
    byte[] bytes = new byte[NumberChars / 2];
    for (int i = 0; i < NumberChars; i += 2)
        bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
    return bytes;
}

public static async Task<HttpResponseMessage> hexy2webmAsync(string url)
{
    url = url.Replace("\"", "&quot;");
    HttpClient cli = new HttpClient();
    cli.DefaultRequestHeaders.Add(
        "x-functions-key", 
        "klucz_api_do_funkcji_w_pythonie"
        );
    StringContent strc = new StringContent(
        $"{{\"rurl\": \"{url}\"}}",
        Encoding.UTF8,
        "application/json"
        );
    var resp = await cli.PostAsync("adres_www_funkcji_w_pythonie", strc);
    if (resp.IsSuccessStatusCode)
    {
        HttpResponseMessage webmRes = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
        
        int len = 4096;
        byte[] buff = new byte[len];
        MemoryStream ms = new MemoryStream();

        using (BinaryReader r = new BinaryReader(await resp.Content.ReadAsStreamAsync()))
        {
            // strumień zaczyna się od apostrofa
            if ('"' != r.ReadChar()) throw new Exception("err fmt 1");

            int c;
            while (0 < (c = r.Read(buff, 0, len)))
            {
                // strumień zakończy się apostrofem dlatego taki skrót myślowy
                if (1 == c % 2)
                    c--;
                string hexy = Encoding.ASCII.GetString(buff, 0, c);
                byte[] data = StringToByteArray(hexy);
                ms.Write(data, 0, data.Length);
            }
        }

        ms.Position = 0;
        webmRes.Content = new StreamContent(ms);
        webmRes.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("audio/webm");
        return webmRes;
    }

    return new HttpResponseMessage(System.Net.HttpStatusCode.ServiceUnavailable);
}

Odtwarzacz muzyki w HTML5

Pod Chrome lub Edge można napisać prosty odtwarzacz korzystając z tagu <audio/> choć poniższa odpowiedź z przeglądarki nie zachęca

// zmienna 'audio' to referencja na element DOM audio
> audio.canPlayType("audio/webm")
"maybe"

Pomijając szczegóły warstwy prezentacji w HTML warto nakreślić tylko sposoby pobierania muzyki - bezpośrednie albo za pomocą zapytań AJAXowych w JavaScript (Blob lub ArrayBuffer)

var api = document.querySelector("#api"),
    music = document.querySelector("#music"),
    audio = document.querySelector("#player"),
    load = document.querySelector("#load"),
    ctx = new (window.AudioContext || window.webkitAudioContext)();

var gArrayBuffer = undefined,
    gBlobBuffer = undefined,
    gBlobUrl = undefined;

var audioUrl1 = 'https://....azurewebsites.net/api/fn_name?vid={URL_FILMU}';
var audioUrl2 = 'http://upload.wikimedia.org/wikipedia/commons/a/aa/White_noise.ogg';

var source = ctx.createBufferSource();

function xhr(url, dataType, cb) {        
    var x = new XMLHttpRequest();
    x.open('GET', url, true);
    x.responseType = dataType; // blob|arraybuffer
    x.onload = function _onload() {
        cb(null, x.response);
    };
    x.onerror = function _onerror() {
        cb('err');
    }
    try {
        x.send();
    } catch (err) {
    }
}

function loadBackgroundAudio(audioSource) {
    xhr(audioUrl1, 'arraybuffer', function _bgAudio(err, buffer) {
        if (err) 
            return;
        ctx.decodeAudioData(buffer, function _decodeCb(audioBuffer) {
            gArrayBuffer = audioBuffer;
            audioSource.buffer = audioBuffer;
            audioSource.connect(ctx.destination);
            //audioSource.loop = true;
            audioSource.start(0); // Failed to execute 'start' on 'AudioBufferSourceNode': cannot call start more than once.
        });
    });
}

function buggedChromeBlob(audioControl, audioUrl, cb) {
    xhr(audioUrl, 'blob', function _chrBlob(err, blob) {
        if (err) {
            if (cb) cb();
            alert('err');
            return;
        }
        if (gBlobUrl) {
            URL.revokeObjectURL(gBlobUrl);
            gBlobUrl = undefined;
        }
        gBlobBuffer = blob;
        gBlobUrl = URL.createObjectURL(blob);
        setTimeout(function _tout() {
            audioControl.src = gBlobUrl;
            //audioControl.load();
            audioControl.play();
            if (cb) cb();
        },10); // setTimeout rozwiązuje problem z nieodtwarzaniem WEBM w Chrome. W Edge jest ok.
    });
}

//loadBackgroundAudio(source);
buggedChromeBlob(audio, audioUrl1);

Wystawianie odtwarzacza HTML na świat

Najprostszym sposobem umieszczenia odtwarzacza jest stworzenie strony internetowej w pliku .html i wrzucenie jej jako blob przez Azure Storage.

W takim przypadku należy pamiętać aby adres hosta dodać do CORS funkcji Azure bo inaczej przeglądarka będzie odrzucać odpowiedzi jako nie posiadające prawidłowego nagłówka Access-Control-Allow-Origin. Ustawienie to znajduje się na zakładce Platform features i opcji CORS.

Miłego słuchania w trybie offline!

... po umieszczeniu w/w treści na swoim koncie Azure.