Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-introduce ability to change quality for progressive download sources (e.g. mp4 files) #352

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions src/js/core/StreamProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -393,25 +393,34 @@ export default class SteramProvider extends PlayerResource {

const qualities = await player.getQualities();
const total = qualities.length;
let index = -1;
qualities.some((q,i) => {
if (quality.index === q.index) {
index = i;
}
return index !== -1;
});
const index = qualities.findIndex(q => q.index === quality.index);

if (index>=0) {
const qualityFactor = index / total;
for (const content in this.streams) {
const stream = this.streams[content];

// If the video is currently playing and some of the players do not
// support changing the quality while playing, we need to pause.
const pauseRequired = !await player.paused() && Object.values(this.streams)
.some(stream => stream.player.qualityChangeNeedsPause);
if (pauseRequired) {
await this.pause();
}

// Change the quality for each separate stream. This waits until all
// streams have loaded the video far enough to start playing.
await Promise.all(Object.values(this.streams).map(async (stream) => {
const streamQualities = (await stream.player.getQualities()) || [];
this.player.log.debug(streamQualities);
this.player.log.debug("Stream qualities:", streamQualities);
if (streamQualities.length>1) {
const qualityIndex = Math.round(streamQualities.length * qualityFactor);
const selectedQuality = streamQualities[qualityIndex];
await stream.player.setQuality(selectedQuality);
}
}));

// If we paused before changing the quality, we have to resume now as well.
if (pauseRequired) {
await this.play();
}
}
}
Expand All @@ -431,4 +440,4 @@ export default class SteramProvider extends PlayerResource {
get currentAudioTrack() {
return this.mainAudioPlayer.currentAudioTrack;
}
}
}
119 changes: 106 additions & 13 deletions src/js/videoFormats/es.upv.paella.mp4VideoFormat.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import VideoPlugin, { Video } from 'paella-core/js/core/VideoPlugin';
import { resolveResourcePath } from 'paella-core/js/core/utils';
import PaellaCoreVideoFormats from './PaellaCoreVideoFormats';
import VideoQualityItem from 'paella-core/js/core/VideoQualityItem';

let video = null;

Expand Down Expand Up @@ -33,10 +34,6 @@ export class Mp4Video extends Video {

this.isMainAudio = isMainAudio;

// Autoplay is required to play videos in some browsers
this.element.setAttribute("autoplay","");
this.element.autoplay = true;

// The video is muted by default, to allow autoplay to work
if (!isMainAudio) {
this.element.muted = true;
Expand Down Expand Up @@ -164,16 +161,58 @@ export class Mp4Video extends Video {
}

async getQualities() {
// TODO: implement this
if (!this._qualities) {
this._qualities = this._sources.map((src, i) => new VideoQualityItem({
index: i,
label: `${src.res.w}x${src.res.h}`,
shortLabel: `${src.res.h}p`,
width: src.res.w,
height: src.res.h,
src: src.src,
}));
}

return this._qualities;
}

async setQuality(/* q */) {
// TODO: implement this
get qualityChangeNeedsPause() {
return true;
}

async setQuality(q) {
if (!(q instanceof VideoQualityItem)) {
throw new Error('Invalid parameter setting video quality');
}

this.player.log.debug(`org.opencast.paella.mp4MultiQualityVideoFormat: Change video quality to ${q.shortLabel}`);

// Clear data, set the `src` attribute to the new video file and then
// set some values to previous values.
const currentTime = this.video.currentTime;
const playbackRate = this.video.playbackRate;
this.clearStreamData();
this.video.autoplay = true;
this.video.src = q.src;
this.video.currentTime = currentTime;
this.video.playbackRate = playbackRate;
this.video.addEventListener('ended', this._endedCallback);
this._currentQuality = q.index;

// Wait for the `canplay` event to know that the video has loaded sufficiently.
await new Promise(resolve => {
const f = () => {
this._ready = true;
this.video.autoplay = false;
this.video.pause();
this.video.removeEventListener('canplay', f);
resolve(null);
};
this.video.addEventListener('canplay', f);
});
}

get currentQuality() {
// TODO: implement this
return 0;
return this._qualities[this._currentQuality];
}

async getDimensions() {
Expand Down Expand Up @@ -211,7 +250,47 @@ export class Mp4Video extends Video {
this._sources.sort((a,b) => {
return Number(a.res.w) - Number(b.res.w);
});
this._currentQuality = this._sources.length - 1;


// Select a fitting initial quality
const screenRes = [window.screen.width, window.screen.height]
.map(x => x * window.devicePixelRatio);
let screenMin = Math.min(screenRes[0], screenRes[1]);
let screenMax = Math.max(screenRes[0], screenRes[1]);

// This is the test recommended by MDN:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
//
// Of course, ideally one wouldn't have to look at the user agent string
// at all and would get the information one wants via different means.
// But for what we want to query, there are no different means. The
// `devicePixelRatio` helps only marginally. Network speed information
// API is still unstable. Since this is only the initial quality and
// this test works well the vast majority of times, it makes sense to
// just use it. We use something between 720p and 1080p as resolution
// target. The YouTube app seems to use 720p as default.
const isMobile = /Mobi/i.test(window.navigator.userAgent);
if (isMobile) {
screenMin = Math.max(screenMin, 900);
screenMax = Math.max(screenMin, 1600);
}

// Find the largest video that still fully fits inside the screen. The
// array is already sorted in ascending order. Note that we only
// compare the minimums and maximums to not run into landscape vs.
// portrait mode problems. Ideally, we would change the quality when
// the device is turned, but that would be way more involved.
let quality = 0;
for (let i = 1; i < this._sources.length; i += 1) {
const src = this._sources[i];
const srcMin = Math.min(src.res.w, src.res.h);
const srcMax = Math.max(src.res.w, src.res.h);
if (srcMin <= screenMin && srcMax <= screenMax) {
quality = i;
}
}

this._currentQuality = quality;
this._currentSource = this._sources[this._currentQuality];

if (!this.isMainAudioPlayer) {
Expand All @@ -225,6 +304,11 @@ export class Mp4Video extends Video {
}
}

// Set `autoplay` as this improves loading the video, for some reason.
this.video.setAttribute("autoplay","");
this.video.autoplay = true;
const wasMuted = this.video.muted;
this.video.muted = true;
this.video.src = resolveResourcePath(this.player, this._currentSource.src);
this._endedCallback = this._endedCallback || (() => {
if (typeof(this._videoEndedCallback) == "function") {
Expand All @@ -233,7 +317,17 @@ export class Mp4Video extends Video {
});
this.video.addEventListener("ended", this._endedCallback);

await this.waitForLoaded();
// Wait until the video has loaded to play at least a bit.
await new Promise(resolve => {
const f = () => {
this.video.removeEventListener('canplay', f);
this.video.autoplay = false;
this.video.muted = wasMuted;
this.video.pause();
resolve();
};
this.video.addEventListener('canplay', f);
});

this.player.log.debug(`es.upv.paella.mp4VideoFormat (${ this.streamData.content }): video loaded and ready.`);
this.saveDisabledProperties(this.video);
Expand Down Expand Up @@ -277,7 +371,6 @@ export class Mp4Video extends Video {
reject(new Error(this.player.translate("Error loading video: $1. Code: $2 $3", [this.video.src, this.video.error, this.video.error.message])));
}
else if (this.video.readyState >= 2) {
this.video.pause(); // Pause the video because it is loaded in autoplay mode
this._ready = true;
resolve();
}
Expand Down Expand Up @@ -326,4 +419,4 @@ export default class Mp4VideoPlugin extends VideoPlugin {
}))
};
}
}
}
Loading