Skip to content

Commit

Permalink
Fixed issues with ESPN and FOX non-4K streams
Browse files Browse the repository at this point in the history
  • Loading branch information
m0ngr31 committed Feb 15, 2023
1 parent 0269e85 commit 6c0d2b5
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 147 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<img src="https://i.imgur.com/FIGZdR3.png">
</p>

Current version: **2.0.7**
Current version: **2.0.8**

# About
This takes ESPN/ESPN+, FOX Sports, and NBC Sports programming and transforms it into a "live TV" experience with virtual linear channels. It will discover what is on, and generate a schedule of channels that will give you M3U and XMLTV files that you can import into something like [Jellyfin](https://jellyfin.org), [Channels](https://getchannels.com), or [xTeVe](https://github.com/xteve-project/xTeVe).
Expand Down
40 changes: 9 additions & 31 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "eplustv",
"version": "2.0.7",
"version": "2.0.8",
"description": "",
"main": "index.js",
"scripts": {
Expand All @@ -13,9 +13,9 @@
"license": "MIT",
"dependencies": {
"axios": "^1.2.2",
"dynamic-hls-proxy": "^1.1.21",
"express": "^4.17.1",
"fs-extra": "^10.0.0",
"hls-parser": "^0.10.6",
"jwt-decode": "^3.1.2",
"lodash": "^4.17.21",
"moment": "^2.29.1",
Expand Down
141 changes: 28 additions & 113 deletions services/playlist-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Chunklist, Playlist, RenditionSortOrder} from 'dynamic-hls-proxy';
import HLS from 'hls-parser';
import axios from 'axios';
import _ from 'lodash';

Expand Down Expand Up @@ -34,28 +34,6 @@ const isBase64Uri = (url: string) => url.indexOf('base64') > -1 || url.startsWit
const PROXY_SEGMENTS =
process.env.PROXY_SEGMENTS && process.env.PROXY_SEGMENTS.toLowerCase() !== 'false' ? true : false;

const VALID_RESOLUTIONS = ['UHD/HDR', 'UHD/SDR', '1080p', '720p', '540p'];

const getMaxRes = _.memoize((): string =>
_.includes(VALID_RESOLUTIONS, process.env.MAX_RESOLUTION) ? process.env.MAX_RESOLUTION : 'UHD/SDR',
);

const getResolutionRanges = _.memoize((): [number, number] => {
const setProfile = getMaxRes();

switch (setProfile) {
case 'UHD/HDR':
case 'UHD/SDR':
return [0, 2160];
case '1080p':
return [0, 1080];
case '720p':
return [0, 720];
default:
return [0, 540];
}
});

const reTarget = /#EXT-X-TARGETDURATION:([0-9]+)/;
const reAudioTrack = /#EXT-X-MEDIA:TYPE=AUDIO.*URI="(.*[^,])"/gm;
const reMap = /#EXT-X-MAP:URI="(.*[^,])"/gm;
Expand All @@ -77,6 +55,13 @@ const getTargetDuration = (chunklist: string, divide = true): number => {
return targetDuration;
};

const parseReplacementUrl = (url: string, manifestUrl: string): string =>
isRelativeUrl(url)
? usesHostRoot(url)
? convertHostUrl(url, manifestUrl)
: cleanUrl(`${createBaseUrl(manifestUrl)}/${url}`)
: url;

export class PlaylistHandler {
public playlist: string;

Expand Down Expand Up @@ -108,8 +93,6 @@ export class PlaylistHandler {
}

public async parseManifest(manifestUrl: string, headers: IHeaders): Promise<void> {
const [hMin, hMax] = getResolutionRanges();

try {
const {data: manifest, request} = await axios.get<string>(manifestUrl, {
headers: {
Expand All @@ -119,108 +102,40 @@ export class PlaylistHandler {
});

const realManifestUrl = request.res.responseUrl;
const urlParams = new URL(realManifestUrl).search;
const urlParams = this.network === 'foxsports' ? new URL(realManifestUrl).search : '';

let updatedManifest = manifest;

const playlist = Playlist.loadFromString(manifest);
const renditions = playlist.getRenditions();

playlist.setResolutionRange(hMin, hMax);

playlist
.sortByBandwidth(getMaxRes() === '540p' ? RenditionSortOrder.nonHdFirst : RenditionSortOrder.bestFirst)
.setLimit(1);

// let chunklist: string;

// try {
// chunklist = decodeURIComponent(playlist.getVideoRenditionUrl(0));
// } catch (e) {
// chunklist = decodeURIComponent(playlist.getRenditions().audioRenditions[0].uri);
// }
const playlist = HLS.parse(manifest);

/** For FOX 4K streams */
const audioTracks = [...manifest.matchAll(reAudioTrack)];

audioTracks.forEach(track => {
if (track && track[1]) {
const fullChunklistUrl = cleanUrl(
isRelativeUrl(track[1])
? usesHostRoot(track[1])
? convertHostUrl(track[1], realManifestUrl)
: `${createBaseUrl(realManifestUrl)}/${track[1]}`
: track[1],
);
const fullChunklistUrl = parseReplacementUrl(track[1], realManifestUrl);

const chunklistName = cacheLayer.getChunklistFromUrl(`${fullChunklistUrl}${urlParams}`);
updatedManifest = updatedManifest.replace(track[1], `${this.baseProxyUrl}${chunklistName}.m3u8`);
}
});

const subTracks = [...manifest.matchAll(reSubMap)];

subTracks.forEach(track => {
if (track && track[1]) {
const fullChunklistUrl = cleanUrl(
isRelativeUrl(track[1])
? usesHostRoot(track[1])
? convertHostUrl(track[1], realManifestUrl)
: `${createBaseUrl(realManifestUrl)}/${track[1]}`
: track[1],
);
const fullChunklistUrl = parseReplacementUrl(track[1], realManifestUrl);

const chunklistName = cacheLayer.getChunklistFromUrl(`${fullChunklistUrl}${urlParams}`);
updatedManifest = updatedManifest.replace(track[1], `${this.baseProxyUrl}${chunklistName}.m3u8`);
}
});

/**
* For some reason this library picks up 4K FOX Sports streams as
* Audio renditions instead of video. So we have to check these too
*/
renditions.audioRenditions.forEach(rendition => {
// if (decodeURIComponent(rendition.uri) !== chunklist) {
// // updatedManifest = updatedManifest.replace(decodeURI(rendition.uri), '');
// } else {
const fullChunklistUrl = cleanUrl(
isRelativeUrl(rendition.uri)
? usesHostRoot(rendition.uri)
? convertHostUrl(rendition.uri, realManifestUrl)
: `${createBaseUrl(realManifestUrl)}/${rendition.uri}`
: rendition.uri,
);

const chunklistName = cacheLayer.getChunklistFromUrl(`${fullChunklistUrl}${urlParams}`);
updatedManifest = updatedManifest.replace(rendition.uri, `${this.baseProxyUrl}${chunklistName}.m3u8`);
// }
});

renditions.videoRenditions.forEach(rendition => {
// if (decodeURIComponent(rendition.uri) !== chunklist) {
// // updatedManifest = updatedManifest.replace(decodeURI(rendition.uri), '');
// } else {
const fullChunklistUrl = cleanUrl(
isRelativeUrl(rendition.uri)
? usesHostRoot(rendition.uri)
? convertHostUrl(rendition.uri, realManifestUrl)
: `${createBaseUrl(realManifestUrl)}/${rendition.uri}`
: rendition.uri,
);
playlist.variants.forEach(variant => {
const fullChunklistUrl = parseReplacementUrl(variant.uri, realManifestUrl);

const chunklistName = cacheLayer.getChunklistFromUrl(`${fullChunklistUrl}${urlParams}`);
updatedManifest = updatedManifest.replace(rendition.uri, `${this.baseProxyUrl}${chunklistName}.m3u8`);
// }
updatedManifest = updatedManifest.replace(variant.uri, `${this.baseProxyUrl}${chunklistName}.m3u8`);
});

// Cleanup m3u8
// updatedManifest = updatedManifest
// .replace(/#UPLYNK-MEDIA.*$/gm, '')
// .replace(/#EXT-X-I-FRAME-STREAM-INF.*$/gm, '')
// .replace(/#EXT-X-IMAGE-STREAM-INF.*$/gm, '')
// .replace(/#EXT-X-STREAM-INF.*$\n\n/gm, '')
// .replace(/\n\n/gm, '\n');

this.playlist = updatedManifest;
} catch (e) {
console.error(e);
Expand All @@ -247,21 +162,17 @@ export class PlaylistHandler {
},
});

let updatedChunkList = chunkList;

const realChunklistUrl = request.res.responseUrl;
const baseManifestUrl = cleanUrl(createBaseUrlChunklist(realChunklistUrl, this.network));
const urlParams = new URL(realChunklistUrl).search;

if (!this.segmentDuration) {
this.segmentDuration = getTargetDuration(chunkList);
}

let updatedChunkList = chunkList;
const keys = new Set<string>();
const chunks = Chunklist.loadFromString(chunkList);

const chunks = HLS.parse(chunkList);

chunks.segments.forEach(segment => {
const segmentUrl: string = segment.segment.uri;
const segmentKey = segment.segment.key?.uri;
const segmentUrl = segment.uri;
const segmentKey = segment.key?.uri;

const fullSegmentUrl = isRelativeUrl(segmentUrl)
? usesHostRoot(segmentUrl)
Expand All @@ -278,25 +189,29 @@ export class PlaylistHandler {
// Just until I figure out a workaround
!segmentUrl.endsWith('mp4')
) {
const segmentName = cacheLayer.getSegmentFromUrl(`${fullSegmentUrl}${urlParams}`, `${this.channel}-segment`);
const segmentName = cacheLayer.getSegmentFromUrl(fullSegmentUrl, `${this.channel}-segment`);
updatedChunkList = updatedChunkList.replace(segmentUrl, `${this.baseUrl}${segmentName}.ts`);
} else {
updatedChunkList = updatedChunkList.replace(segmentUrl, `${fullSegmentUrl}${urlParams}`);
updatedChunkList = updatedChunkList.replace(segmentUrl, fullSegmentUrl);
}

if (segmentKey && !isBase64Uri(segmentKey)) {
keys.add(segmentKey);
}
});

if (!this.segmentDuration) {
this.segmentDuration = getTargetDuration(chunkList);
}

keys.forEach(key => {
const fullKeyUrl = isRelativeUrl(key)
? usesHostRoot(key)
? convertHostUrl(key, baseManifestUrl)
: cleanUrl(`${baseManifestUrl}${key}`)
: key;

const keyName = cacheLayer.getSegmentFromUrl(`${fullKeyUrl}${urlParams}`, `${this.channel}-key`);
const keyName = cacheLayer.getSegmentFromUrl(fullKeyUrl, `${this.channel}-key`);

while (updatedChunkList.indexOf(key) > -1) {
updatedChunkList = updatedChunkList.replace(key, `${this.baseUrl}${keyName}.key`);
Expand Down

0 comments on commit 6c0d2b5

Please sign in to comment.