4. Distribució - maarcnavarro9/WorldAnimals GitHub Wiki

Processament de vídeos per especificar-ne els keyframes

ffmpeg -i WorldAnimalsV1_4K.mp4 -filter_complex "[0:v]split=4[v1][v2][v3][v4]; [v1]scale=w=3840:h=2160[v1out]; [v2]scale=w=1920:h=1080[v2out]; [v3]scale=w=1280:h=720[v3out]; [v4]scale=w=640:h=360[v4out]" -map "[v1out]" -c:v libx264 -crf 23 -b:v 15M -maxrate 15M -bufsize 30M -preset slow -g 30 -sc_threshold 0 -keyint_min 30 video_4k.mp4 -map "[v2out]" -c:v libx264 -crf 23 -b:v 7M -maxrate 7M -bufsize 14M -preset slow -g 30 -sc_threshold 0 -keyint_min 30 video_1080p.mp4 -map "[v3out]" -c:v libx264 -crf 23 -b:v 3M -maxrate 3M -bufsize 6M -preset slow -g 30 -sc_threshold 0 -keyint_min 30 video_720p.mp4 -map "[v4out]" -c:v libx264 -crf 23 -b:v 1M -maxrate 1M -bufsize 2M -preset slow -g 30 -sc_threshold 0 -keyint_min 30 video_360p.mp4 -map a:0 -c:a aac -b:a 320k -ac 2 audio_320k.aac -map a:0 -c:a aac -b:a 128k -ac 2 audio_128k.aac

ffmpeg -i WorldAnimalsV2_4K.mp4 -filter_complex "[0:v]split=4[v1][v2][v3][v4]; [v1]scale=w=3840:h=2160[v1out]; [v2]scale=w=1920:h=1080[v2out]; [v3]scale=w=1280:h=720[v3out]; [v4]scale=w=640:h=360[v4out]" -map "[v1out]" -c:v libx264 -crf 23 -b:v 15M -maxrate 15M -bufsize 30M -preset slow -g 30 -sc_threshold 0 -keyint_min 30 video_4k.mp4 -map "[v2out]" -c:v libx264 -crf 23 -b:v 7M -maxrate 7M -bufsize 14M -preset slow -g 30 -sc_threshold 0 -keyint_min 30 video_1080p.mp4 -map "[v3out]" -c:v libx264 -crf 23 -b:v 3M -maxrate 3M -bufsize 6M -preset slow -g 30 -sc_threshold 0 -keyint_min 30 video_720p.mp4 -map "[v4out]" -c:v libx264 -crf 23 -b:v 1M -maxrate 1M -bufsize 2M -preset slow -g 30 -sc_threshold 0 -keyint_min 30 video_360p.mp4 -map a:0 -c:a aac -b:a 320k -ac 2 audio_320k.aac -map a:0 -c:a aac -b:a 128k -ac 2 audio_128k.aac

Streaming adaptatiu

mp4box -dash 4000 -profile onDemand .\video_4k.mp4 .\video_1080p.mp4 .\video_720p.mp4 .\video_360p.mp4 .\audio_320k.aac .\audio_128k.aac -out out/manifest.mpd --dual --cmaf

1. DASH

<video style="width: 800px" controls></video>
<script src="//cdn.dashjs.org/latest/dash.all.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
 init();
});
function init() {
 var video, player, url = "media/manifest.mpd";
 video = document.querySelector("video");
 player = dashjs.MediaPlayer().create();
 player.initialize(video, url, true);
}
</script>

2. HLS

<video style="width: 800px" controls></video>
<script src="//cdn.jsdelivr.net/npm/hls.js@1"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
 init();
});
function init() {
 var video, player, url = "media/mp4box/manifest.m3u8";
 if (Hls.isSupported()) {
 video = document.querySelector('video’);
 player = new Hls();
 player.on(Hls.Events.MEDIA_ATTACHED, function () { /* ... */ });
 player.loadSource(url);
 player.attachMedia(video);
 }
}
</script>

3. BLOCKCHAIN

S'han pujat els dos vídeos a Theta Edgecloud

https://media.thetavideoapi.com/org_0dw11am36pv5x68scfkm93r7i4tf/srvacc_0dbmv8c5m1k237dgvq6ijpvp1/video_pgwzbj7iiw72e9jg8mpvsgs49x/master.m3u8

https://media.thetavideoapi.com/org_0dw11am36pv5x68scfkm93r7i4tf/srvacc_0dbmv8c5m1k237dgvq6ijpvp1/video_dxdd6prg3274ip9uwtpv0t2umh/master.m3u8

4. INFORMACIÓ EXTRA

Creació dinàmica de les qualitats a partir del manifest

Video

Omple el selector de qualitats de vídeo disponibles per DASH

function populateDashQualities() {
    const sel = document.getElementById('qualitySelector');
    sel.querySelectorAll('option:not([value="-1"])').forEach(o => o.remove());
    dashPlayer.getRepresentationsByType('video').forEach((b, i) => {
        const o = document.createElement('option');
        o.value = i;
        o.text = `${b.height}p`;
        sel.appendChild(o);
    });
}

Omple el selector de qualitats de vídeo disponibles per HLS

function populateHlsQualities() {
    const sel = document.getElementById('qualitySelector');
    sel.querySelectorAll('option:not([value="-1"])').forEach(o => o.remove());
    hlsPlayer.levels.forEach((lvl, i) => {
        const o = document.createElement('option');
        o.value = i;
        o.text = `${lvl.height}p`;
        sel.appendChild(o);
    });
}

image

Audio

Omple el selector d'àudio amb les diferents pistes disponibles en DASH

function populateDashAudioQualities() {
    const sel = document.getElementById('audioSelector');
    sel.querySelectorAll('option:not([value="-1"])').forEach(o => o.remove());
    dashPlayer.getRepresentationsByType('audio').forEach((b, i) => {
        const o = document.createElement('option');
        o.value = i;
        // Solo mostrar Hz si está definido
        o.text = b.audioSamplingRate
            ? `${b.audioSamplingRate} Hz — ${(b.bandwidth / 1000).toFixed(0)} kbps`
            : `${(b.bandwidth / 1000).toFixed(0)} kbps`;
        sel.appendChild(o);
    });
}

Omple el selector d'àudio amb les pistes disponibles en HLS

function populateHlsAudioQualities() {
    const sel = document.getElementById('audioSelector');
    sel.querySelectorAll('option:not([value="-1"])').forEach(o => o.remove());
    hlsPlayer.audioTracks.forEach((track, i) => {
        const o = document.createElement('option');
        o.value = i;
        o.text = track.name || `Audio Track ${i + 1}`;
        sel.appendChild(o);
    });
}

image

Canvi de qualitat manualment

Video

Permet canviar manualment la qualitat del vídeo (DASH o HLS)

function changeQuality() {
    const idx = parseInt(document.getElementById('qualitySelector').value, 10);
    if (dashPlayer) {
        dashPlayer.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: idx === -1 } } } });
        if (idx !== -1) {
            dashPlayer.setRepresentationForTypeByIndex('video', idx);
        }
    }
    if (hlsPlayer) {
        hlsPlayer.currentLevel = idx;
    }
}

Audio

Canviar manualment la qualitat/pista d'àudio seleccionada

function changeAudioQuality() {
    const idx = parseInt(document.getElementById('audioSelector').value, 10);
    if (dashPlayer) {
        dashPlayer.updateSettings({ streaming: { abr: { autoSwitchBitrate: { audio: idx === -1 } } } });
        if (idx !== -1) {
            dashPlayer.setRepresentationForTypeByIndex('audio', idx);
        }
        // Mostrar info de la pista seleccionada
        const reps = dashPlayer.getRepresentationsByType('audio');
        console.log('DASH audio seleccionado:', idx, reps[idx]);
    }
    if (hlsPlayer) {
        hlsPlayer.audioTrack = idx;
        // Mostrar info de la pista seleccionada
        const tracks = hlsPlayer.audioTracks;
        console.log('HLS audio seleccionado:', idx, tracks && tracks[idx]);
    }
}

320p

image

4K

image

Gestió dinàmica dels tracks

Afeger pistes de subtítols i metadades al vídeo

function addTracks(video, videoNum) {
    const langs = [
        { code: 'En', label: 'English', srclang: 'en' },
        { code: 'Es', label: 'Spanish', srclang: 'es' },
        { code: 'Ca', label: 'Catalan', srclang: 'ca' }
    ];
    langs.forEach((lang, i) => {
        const tr = document.createElement("track");
        tr.kind = "subtitles";
        tr.label = lang.label;
        tr.srclang = lang.srclang;
        tr.src = `./media/descriptionsV${videoNum}_${lang.code}.vtt`;
        if (i === 0) tr.default = true;
        video.appendChild(tr);
    });
    const meta = document.createElement("track");
    meta.kind = "metadata";
    meta.label = "Metadata";
    meta.src = `./media/metadataV${videoNum}.vtt`;
    video.appendChild(meta);
}

Configurar els tracks de text i activar la gestió de metadades

function setupTracksAndChapters(video, videoNum) {
    addTracks(video, videoNum);
    console.log('➤ Tracks añadidos para vídeo', videoNum);

    const tracks = video.textTracks;
    for (let i = 0; i < tracks.length; i++) {
        const t = tracks[i];
        console.log(`  Track[${i}]: kind=${t.kind} label=${t.label} mode=${t.mode}`);
        if (t.kind === 'subtitles') {
            t.mode = 'disabled';
        }
        if (t.kind === 'metadata') {
            t.mode = 'hidden';
            console.log('    → Metadata track encontrada, cues totales:', t.cues.length);

            t.addEventListener('cuechange', () => {
                console.log('    ✶ cuechange disparado, activeCues=', t.activeCues.length);
                if (t.activeCues.length > 0) {
                    const text = t.activeCues[0].text;
                    let data;
                    try {
                        data = JSON.parse(text);
                    } catch (e) {
                        console.error('      ¡JSON inválido en cue!', text);
                        return;
                    }
                    console.log('      → Metadata:', data);
                    document.getElementById('fotoAnimal').src = data.link_imagen;
                    document.getElementById('nombre').textContent = data.Nombre;
                    document.getElementById('especie').textContent = data.Especie;
                    document.getElementById('descripcion').textContent = data.Descripcion;
                    if (data.coordenadas_geográficas) {
                        const lat = +data.coordenadas_geográficas.latitud;
                        const lng = +data.coordenadas_geográficas.longitud;
                        console.log(`      → Centrar mapa en ${lat},${lng}`);
                        mapa.setCenter({ lat, lng });
                        marcador.setPosition({ lat, lng });
                    }
                    const allMetadata = Array.from(t.cues).map(c => {
                        try { return JSON.parse(c.text); }
                        catch { return null; }
                    }).filter(x => x);
                    console.log('      → Generando botones para', allMetadata.length, 'capítulos');
                    generarBotonesCapitulos(allMetadata);
                }
            });
        }
    }
}

Crear botons per navegar entre capítols definits en les metadades

function generarBotonesCapitulos(data) {
    const container = document.getElementById('capitulos');
    container.innerHTML = '';
    data.forEach((item, i) => {
        const b = document.createElement('button');
        b.textContent = item.Nombre;
        b.onclick = () => {
            const video = document.getElementById('myVideo');
            const metadataTrack = Array.from(video.textTracks).find(track => track.kind === 'metadata');
            if (!metadataTrack) {
                console.error('Pista metadata no encontrada');
                return;
            }
            const cue = metadataTrack.cues[i];
            if (cue) {
                video.currentTime = cue.startTime - 0.001;
                video.play();
            }
        };
        container.appendChild(b);
    });
}
⚠️ **GitHub.com Fallback** ⚠️