Skip to content

Commit

Permalink
Support byte-ranges in hls.js integration
Browse files Browse the repository at this point in the history
  • Loading branch information
mrlika committed Dec 25, 2018
1 parent 88dd5bf commit 530fdfe
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 45 deletions.
15 changes: 10 additions & 5 deletions p2p-media-loader-hlsjs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Returns result from `p2pml.core.HybridLoader.isSupported()`.

Creates a new `Engine` instance.

`settings` structure:
`settings` object with the following fields:
- `segments`
+ `forwardSegmentCount` - Number of segments for building up predicted forward segments sequence; used to predownload and share via P2P. Default is 20;
+ `swarmId` - Override default swarm ID that is used to identify unique media stream with trackers (manifest URL without query parameters is used as the swarm ID if the parameter is not specified);
Expand All @@ -103,11 +103,16 @@ Returns engine instance settings.

Creates hls.js loader class bound to this engine.

### `engine.setPlayingSegment(url)`
### `engine.setPlayingSegment(url, byterange)`

Notifies engine about current playing segment url.
Notifies engine about current playing segment.

Needed for own integrations with other players. If you write one, you should update engine with current playing segment url from your player.
Needed for own integrations with other players. If you write one, you should update engine with current playing segment from your player.

`url` segment URL.
`byterange` segment byte-range object with the fields properties or undefined:
- `offset` segment offset
- `length` segment length

### `engine.destroy()`

Expand All @@ -125,7 +130,7 @@ In order a player to be able to integrate with the Engine, it should meet follow
3. Player allows to subcribe to events on hls.js player.
- If player exposes `hls` object, you just call `p2pml.hlsjs.initHlsJsPlayer(hls)`;
- Or if player allows to directly subsctibe to hls.js events, you need to handle:
+ `hlsFragChanged` - call `engine.setPlayingSegment(url)` to notify Engine about current playing segment url;
+ `hlsFragChanged` - call `engine.setPlayingSegment(url, byterange)` to notify Engine about current playing segment url;
+ `hlsDestroying` - call `engine.destroy()` to inform Engine about destroying hls.js player;

### `initHlsJsPlayer(player)`
Expand Down
7 changes: 3 additions & 4 deletions p2p-media-loader-hlsjs/lib/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import {EventEmitter} from "events";
import {Events, LoaderInterface, HybridLoader} from "p2p-media-loader-core";
import {SegmentManager} from "./segment-manager";
import {SegmentManager, Byterange} from "./segment-manager";
import {HlsJsLoader} from "./hlsjs-loader";
import {createHlsJsLoaderClass} from "./hlsjs-loader-class";

Expand Down Expand Up @@ -56,8 +56,7 @@ export class Engine extends EventEmitter {
};
}

public setPlayingSegment(url: string) {
this.segmentManager.setPlayingSegment(url);
public setPlayingSegment(url: string, byterange: Byterange) {
this.segmentManager.setPlayingSegment(url, byterange);
}

}
8 changes: 7 additions & 1 deletion p2p-media-loader-hlsjs/lib/hlsjs-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export class HlsJsLoader {
.catch((error: any) => this.error(error, context, callbacks));
} else if (context.frag) {
this.segmentManager.loadSegment(context.url,
(context.rangeStart == undefined) || (context.rangeEnd == undefined)
? undefined
: { offset: context.rangeStart, length: context.rangeEnd - context.rangeStart },
(content: ArrayBuffer, downloadSpeed: number) => setTimeout(() => this.successSegment(content, downloadSpeed, context, callbacks), 0),
(error: any) => setTimeout(() => this.error(error, context, callbacks), 0)
);
Expand All @@ -43,7 +46,10 @@ export class HlsJsLoader {
}

public abort(context: any): void {
this.segmentManager.abortSegment(context.url);
this.segmentManager.abortSegment(context.url,
(context.rangeStart == undefined) || (context.rangeEnd == undefined)
? undefined
: { offset: context.rangeStart, length: context.rangeEnd - context.rangeStart });
}

private successPlaylist(content: string, context: any, callbacks: any): void {
Expand Down
17 changes: 13 additions & 4 deletions p2p-media-loader-hlsjs/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,17 @@ export function initVideoJsContribHlsJsPlayer(player: any): void {

export function initMediaElementJsPlayer(mediaElement: any): void {
mediaElement.addEventListener("hlsFragChanged", (event: any) => {
const url = event.data && event.data.length > 1 && event.data[ 1 ].frag ? event.data[ 1 ].frag.url : undefined;
const hls = mediaElement.hlsPlayer;
if (hls && hls.config && hls.config.loader && typeof hls.config.loader.getEngine === "function") {
const engine: Engine = hls.config.loader.getEngine();
engine.setPlayingSegment(url);

if (event.data && (event.data.length > 1)) {
const frag = event.data[1].frag;
const byterange = (frag.byteRange.length !== 2)
? undefined
: { offset: frag.byteRange[0], length: frag.byteRange[1] - frag.byteRange[0] };
engine.setPlayingSegment(frag.url, byterange);
}
}
});
mediaElement.addEventListener("hlsDestroying", () => {
Expand All @@ -79,8 +85,11 @@ export const version = typeof(__P2PML_VERSION__) === "undefined" ? "__VERSION__"

function initHlsJsEvents(player: any, engine: Engine): void {
player.on("hlsFragChanged", function (event_unused: any, data: any) {
const url = data && data.frag ? data.frag.url : undefined;
engine.setPlayingSegment(url);
const frag = data.frag;
const byterange = (frag.byteRange.length !== 2)
? undefined
: { offset: frag.byteRange[0], length: frag.byteRange[1] - frag.byteRange[0] };
engine.setPlayingSegment(frag.url, byterange);
});
player.on("hlsDestroying", function () {
engine.destroy();
Expand Down
82 changes: 55 additions & 27 deletions p2p-media-loader-hlsjs/lib/segment-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ const defaultSettings: Settings = {
swarmId: undefined,
};

export type Byterange = { length: number, offset: number } | undefined;

export class SegmentManager {

private loader: LoaderInterface;
private masterPlaylist: Playlist | null = null;
private variantPlaylists: Map<string, Playlist> = new Map();
private segmentRequest: SegmentRequest | null = null;
private playQueue: {segmentSequence: number, segmentUrl: string}[] = [];
private playQueue: {segmentSequence: number, segmentUrl: string, segmentByterange: Byterange}[] = [];
private readonly settings: Settings;

public constructor(loader: LoaderInterface, settings: any = {}) {
Expand Down Expand Up @@ -72,10 +74,10 @@ export class SegmentManager {
return content;
}

public loadSegment(url: string, onSuccess: (content: ArrayBuffer, downloadSpeed: number) => void, onError: (error: any) => void): void {
const segmentLocation = this.getSegmentLocation(url);
public loadSegment(url: string, byterange: Byterange, onSuccess: (content: ArrayBuffer, downloadSpeed: number) => void, onError: (error: any) => void): void {
const segmentLocation = this.getSegmentLocation(url, byterange);
if (!segmentLocation) {
Utils.fetchContentAsArrayBuffer(url)
Utils.fetchContentAsArrayBuffer(url, byterangeToString(byterange))
.then((content: ArrayBuffer) => onSuccess(content, 0))
.catch((error: any) => onError(error));
return;
Expand All @@ -96,21 +98,24 @@ export class SegmentManager {
this.segmentRequest.onError("Cancel segment request: simultaneous segment requests are not supported");
}

this.segmentRequest = new SegmentRequest(url, segmentSequence, segmentLocation.playlist.url, onSuccess, onError);
this.playQueue.push({segmentUrl: url, segmentSequence: segmentSequence});
this.segmentRequest = new SegmentRequest(url, byterange, segmentSequence, segmentLocation.playlist.url, onSuccess, onError);
this.playQueue.push({segmentUrl: url, segmentByterange: byterange, segmentSequence: segmentSequence});
this.loadSegments(segmentLocation.playlist, segmentLocation.segmentIndex, true);
}

public setPlayingSegment(url: string): void {
const urlIndex = this.playQueue.findIndex(segment => segment.segmentUrl == url);
public setPlayingSegment(url: string, byterange: Byterange): void {
const urlIndex = this.playQueue.findIndex(segment =>
(segment.segmentUrl == url) && compareByterange(segment.segmentByterange, byterange));

if (urlIndex >= 0) {
this.playQueue = this.playQueue.slice(urlIndex);
this.updateSegments();
}
}

public abortSegment(url: string): void {
if (this.segmentRequest && this.segmentRequest.segmentUrl === url) {
public abortSegment(url: string, byterange: Byterange): void {
if (this.segmentRequest && (this.segmentRequest.segmentUrl === url) &&
compareByterange(this.segmentRequest.segmentByterange, byterange)) {
this.segmentRequest = null;
}
}
Expand All @@ -133,43 +138,47 @@ export class SegmentManager {
return;
}

const segmentLocation = this.getSegmentLocation(this.segmentRequest.segmentUrl);
const segmentLocation = this.getSegmentLocation(this.segmentRequest.segmentUrl, this.segmentRequest.segmentByterange);
if (segmentLocation) {
this.loadSegments(segmentLocation.playlist, segmentLocation.segmentIndex, false);
} else { // the segment not found in current playlist
const playlist = this.variantPlaylists.get(this.segmentRequest.playlistUrl);
if (playlist) {
this.loadSegments(playlist, 0, false, {
url: this.segmentRequest.segmentUrl,
byterange: this.segmentRequest.segmentByterange,
sequence: this.segmentRequest.segmentSequence});
}
}
}

private onSegmentLoaded = (segment: Segment) => {
if (this.segmentRequest && this.segmentRequest.segmentUrl === segment.url) {
if (this.segmentRequest && (this.segmentRequest.segmentUrl === segment.url) &&
(byterangeToString(this.segmentRequest.segmentByterange) === segment.range)) {
this.segmentRequest.onSuccess(segment.data!.slice(0), segment.downloadSpeed);
this.segmentRequest = null;
}
}

private onSegmentError = (segment: Segment, error: any) => {
if (this.segmentRequest && this.segmentRequest.segmentUrl === segment.url) {
if (this.segmentRequest && (this.segmentRequest.segmentUrl === segment.url) &&
(byterangeToString(this.segmentRequest.segmentByterange) === segment.range)) {
this.segmentRequest.onError(error);
this.segmentRequest = null;
}
}

private onSegmentAbort = (segment: Segment) => {
if (this.segmentRequest && this.segmentRequest.segmentUrl === segment.url) {
if (this.segmentRequest && (this.segmentRequest.segmentUrl === segment.url) &&
(byterangeToString(this.segmentRequest.segmentByterange) === segment.range)) {
this.segmentRequest.onError("Loading aborted: internal abort");
this.segmentRequest = null;
}
}

private getSegmentLocation(url: string): { playlist: Playlist, segmentIndex: number } | undefined {
private getSegmentLocation(url: string, byterange: Byterange): { playlist: Playlist, segmentIndex: number } | undefined {
for (const playlist of this.variantPlaylists.values()) {
const segmentIndex = playlist.getSegmentIndex(url);
const segmentIndex = playlist.getSegmentIndex(url, byterange);
if (segmentIndex >= 0) {
return { playlist: playlist, segmentIndex: segmentIndex };
}
Expand All @@ -178,7 +187,7 @@ export class SegmentManager {
return undefined;
}

private loadSegments(playlist: Playlist, segmentIndex: number, requestFirstSegment: boolean, notInPlaylistSegment?: {url: string, sequence: number}): void {
private loadSegments(playlist: Playlist, segmentIndex: number, requestFirstSegment: boolean, notInPlaylistSegment?: {url: string, byterange: Byterange, sequence: number}): void {
const segments: Segment[] = [];
const playlistSegments: any[] = playlist.manifest.segments;
const initialSequence: number = playlist.manifest.mediaSequence ? playlist.manifest.mediaSequence : 0;
Expand All @@ -189,17 +198,20 @@ export class SegmentManager {
if (notInPlaylistSegment) {
const url = playlist.getSegmentAbsoluteUrl(notInPlaylistSegment.url);
const id = this.getSegmentId(playlist, notInPlaylistSegment.sequence);
segments.push(new Segment(id, url, undefined, priority++));
segments.push(new Segment(id, url, byterangeToString(notInPlaylistSegment.byterange), priority++));

if (requestFirstSegment) {
loadSegmentId = id;
}
}

for (let i = segmentIndex; i < playlistSegments.length && segments.length < this.settings.forwardSegmentCount; ++i) {
const url = playlist.getSegmentAbsoluteUrlByIndex(i);
const segment = playlist.manifest.segments[i];

const url = playlist.getSegmentAbsoluteUrl(segment.uri);
const byterange: Byterange = segment.byterange;
const id = this.getSegmentId(playlist, initialSequence + i);
segments.push(new Segment(id, url, undefined, priority++));
segments.push(new Segment(id, url, byterangeToString(byterange), priority++));

if (requestFirstSegment && !loadSegmentId) {
loadSegmentId = id;
Expand Down Expand Up @@ -236,7 +248,7 @@ export class SegmentManager {
return playlistUrl;
}

} // end of SegmentManager
}

class Playlist {
public swarmId: string = "";
Expand All @@ -248,20 +260,19 @@ class Playlist {
}
}

public getSegmentIndex(url: string): number {
public getSegmentIndex(url: string, byterange: Byterange): number {
for (let i = 0; i < this.manifest.segments.length; ++i) {
if (url === this.getSegmentAbsoluteUrlByIndex(i)) {
const segment = this.manifest.segments[i];
const segmentUrl = this.getSegmentAbsoluteUrl(segment.uri);

if ((url === segmentUrl) && compareByterange(segment.byterange, byterange)) {
return i;
}
}

return -1;
}

public getSegmentAbsoluteUrlByIndex(index: number): string {
return this.getSegmentAbsoluteUrl(this.manifest.segments[index].uri);
}

public getSegmentAbsoluteUrl(segmentUrl: string): string {
return new URL(segmentUrl, this.url).toString();
}
Expand All @@ -270,6 +281,7 @@ class Playlist {
class SegmentRequest {
public constructor(
readonly segmentUrl: string,
readonly segmentByterange: Byterange,
readonly segmentSequence: number,
readonly playlistUrl: string,
readonly onSuccess: (content: ArrayBuffer, downloadSpeed: number) => void,
Expand All @@ -289,3 +301,19 @@ interface Settings {
*/
swarmId: string | undefined;
}

function compareByterange(b1: Byterange, b2: Byterange) {
return (b1 === undefined)
? (b2 === undefined)
: ((b2 !== undefined) && (b1.length === b2.length) && (b1.offset === b2.offset));
}

function byterangeToString(byterange: Byterange): string | undefined {
if (byterange === undefined) {
return undefined;
}

const end = byterange.offset + byterange.length - 1;

return `bytes=${byterange.offset}-${end}`;
}
12 changes: 8 additions & 4 deletions p2p-media-loader-hlsjs/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@

export default class Utils {

public static async fetchContentAsAny(url: string, responseType: XMLHttpRequestResponseType): Promise<any> {
public static async fetchContentAsAny(url: string, range: string | undefined, responseType: XMLHttpRequestResponseType): Promise<any> {
return new Promise<string>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = responseType;

if (range !== undefined) {
xhr.setRequestHeader("Range", range);
}

xhr.onreadystatechange = () => {
if (xhr.readyState !== 4) { return; }
if (xhr.status >= 200 && xhr.status < 300) {
Expand All @@ -36,11 +40,11 @@ export default class Utils {
}

public static async fetchContentAsText(url: string): Promise<string> {
return Utils.fetchContentAsAny(url, "text");
return Utils.fetchContentAsAny(url, undefined, "text");
}

public static async fetchContentAsArrayBuffer(url: string): Promise<ArrayBuffer> {
return Utils.fetchContentAsAny(url, "arraybuffer");
public static async fetchContentAsArrayBuffer(url: string, range: string | undefined): Promise<ArrayBuffer> {
return Utils.fetchContentAsAny(url, range, "arraybuffer");
}

}

0 comments on commit 530fdfe

Please sign in to comment.