From 2ccc3df7667816bdaad7b5abd4429f8986b9a3ad Mon Sep 17 00:00:00 2001 From: Allan Nava Date: Fri, 12 May 2023 13:19:40 +0200 Subject: [PATCH] fix publish --- .github/workflows/lint.yml | 17 ++++ .github/workflows/publish.yml | 104 +++++++++++++++++++++ src/plugins/plugin_barker.ts | 169 ++++++++++++++++++++++++++++++++++ src/plugins/plugin_demo.ts | 109 ++++++++++++++++++++++ src/plugins/plugin_loop.ts | 88 ++++++++++++++++++ 5 files changed, 487 insertions(+) create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/publish.yml create mode 100644 src/plugins/plugin_barker.ts create mode 100644 src/plugins/plugin_demo.ts create mode 100644 src/plugins/plugin_loop.ts diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..d6f41a0 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,17 @@ +name: Linting +on: [pull_request] + +jobs: + lint: + if: "!contains(github.event.pull_request.title, 'WIP!')" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: '16.x' + - name: Install Dependencies + run: npm ci + - name: Run Eslint + run: npm run lint \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..4359aa6 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,104 @@ +name: Build image and push to Docker Hub + +on: + release: + types: [released] + +jobs: + upload_artifacts: + name: Upload artifacts to release + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Upload docker-compose.yml + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: ./docker-compose.yml + asset_name: docker-compose.yml + asset_content_type: text/plain + + push_dockerhub: + name: Push image to Docker Hub + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout + uses: actions/checkout@v3 + # Repo metadata + - name: Repo metadata + id: repo + uses: actions/github-script@v3 + with: + script: | + const repo = await github.repos.get(context.repo) + return repo.data + # Prepare variables + - name: Prepare + id: prep + run: | + REG=ghcr.io + IMAGE=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') + DOCKER_IMAGE=${REG}/${IMAGE} + VERSION=nool + if [ "${{ github.event_name }}" = "schedule" ]; then + VERSION=nightly + elif [[ $GITHUB_REF == refs/tags/* ]]; then + VERSION=${GITHUB_REF#refs/tags/} + elif [[ $GITHUB_REF == refs/heads/* ]]; then + VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -r 's#/+#-#g') + if [ "${{ github.event.repository.default_branch }}" = "$VERSION" ]; then + VERSION=latest + fi + elif [[ $GITHUB_REF == refs/pull/* ]]; then + VERSION=pr-${{ github.event.number }} + fi + TAGS="${DOCKER_IMAGE}:${VERSION}" + if [[ $VERSION =~ ^v[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then + MINOR=${VERSION%.*} + MAJOR=${MINOR%.*} + TAGS="$TAGS,${DOCKER_IMAGE}:${MINOR},${DOCKER_IMAGE}:${MAJOR},${DOCKER_IMAGE}:latest" + fi + echo ::set-output name=version::${VERSION} + echo ::set-output name=tags::${TAGS} + echo ::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ') + # Set up Buildx env + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + # Login + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + # Build image and push to registry + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ steps.prep.outputs.tags }} + labels: | + org.opencontainers.image.title=${{ fromJson(steps.repo.outputs.result).name }} + org.opencontainers.image.description=${{ fromJson(steps.repo.outputs.result).description }} + org.opencontainers.image.url=${{ fromJson(steps.repo.outputs.result).html_url }} + org.opencontainers.image.source=${{ fromJson(steps.repo.outputs.result).html_url }} + org.opencontainers.image.version=${{ steps.prep.outputs.version }} + org.opencontainers.image.created=${{ steps.prep.outputs.created }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.licenses=${{ fromJson(steps.repo.outputs.result).license.spdx_id }} + # + +# \ No newline at end of file diff --git a/src/plugins/plugin_barker.ts b/src/plugins/plugin_barker.ts new file mode 100644 index 0000000..ae01def --- /dev/null +++ b/src/plugins/plugin_barker.ts @@ -0,0 +1,169 @@ +import { + IAssetManager, + IChannelManager, + VodRequest, + VodResponse, + Channel, + ChannelProfile, + IStreamSwitchManager, + Schedule + } from 'eyevinn-channel-engine'; + import { ScheduleStreamType } from 'eyevinn-channel-engine/dist/engine/server'; + import fetch from 'node-fetch'; + import { BasePlugin, PluginInterface } from './interface'; + + import { + generateId, + getDefaultChannelAudioProfile, + getDefaultChannelVideoProfile + } from './utils'; + + class BarkerAssetManager implements IAssetManager { + private fallbackVodToLoop: URL; + + constructor(fallbackVodToLoop: URL) { + this.fallbackVodToLoop = fallbackVodToLoop; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getNextVod(vodRequest: VodRequest): Promise { + const vodResponse = { + id: 'loop', + title: 'VOD on Loop', + uri: this.fallbackVodToLoop.toString() + }; + return vodResponse; + } + } + + class BarkerChannelManager implements IChannelManager { + private channelId: string; + private useDemuxedAudio: boolean; + + constructor(channelId: string, useDemuxedAudio = false) { + this.channelId = channelId; + this.useDemuxedAudio = useDemuxedAudio; + console.log( + `Barker channel available at /channels/${this.channelId}/master.m3u8` + ); + } + + getChannels(): Channel[] { + const channel: Channel = { + id: this.channelId, + profile: this._getProfile() + }; + if (this.useDemuxedAudio) { + channel.audioTracks = getDefaultChannelAudioProfile(); + } + const channelList = [channel]; + return channelList; + } + + _getProfile(): ChannelProfile[] { + return getDefaultChannelVideoProfile(); + } + } + + class BarkerStreamSwitchManager implements IStreamSwitchManager { + private schedule: Schedule[] = []; + private liveStreams: URL[] = []; + private startOffset = -1; + private liveStreamListUrl: URL; + private switchIntervalMs: number = 60 * 1000; + + constructor(liveStreamListUrl: URL, switchIntervalMs?: number) { + this.liveStreamListUrl = liveStreamListUrl; + if (switchIntervalMs) { + this.switchIntervalMs = switchIntervalMs; + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getSchedule(channelId: string): Promise { + if (this.liveStreams.length === 0) { + console.log( + 'Fetching live stream list from ' + this.liveStreamListUrl.toString() + ); + const response = await fetch(this.liveStreamListUrl.toString()); + if (response.ok) { + const body = await response.text(); + this.liveStreams = body + .split(/\r?\n/) + .filter((l) => l !== '') + .map((l) => new URL(l.trim())); + } + } + + const streamDuration = this.switchIntervalMs; + const tsNow = Date.now(); + if (this.startOffset === -1) { + this.startOffset = tsNow; + } + + this.schedule = this.schedule.filter((obj) => obj.end_time >= tsNow); + if (this.schedule.length === 0) { + this.schedule.push({ + eventId: generateId(), + assetId: generateId(), + title: 'Live source A', + type: ScheduleStreamType.LIVE, + start_time: this.startOffset, + end_time: this.startOffset + streamDuration, + uri: this.liveStreams[ + Math.floor(Math.random() * this.liveStreams.length) + ].toString() + }); + this.startOffset += streamDuration; + this.schedule.push({ + eventId: generateId(), + assetId: generateId(), + title: 'Live source B', + type: ScheduleStreamType.LIVE, + start_time: this.startOffset, + end_time: this.startOffset + streamDuration, + uri: this.liveStreams[ + Math.floor(Math.random() * this.liveStreams.length) + ].toString() + }); + this.startOffset += streamDuration; + } + return this.schedule; + } + } + + export class BarkerPlugin extends BasePlugin implements PluginInterface { + constructor() { + super('Barker'); + } + + newAssetManager(): IAssetManager { + const vodToLoop = new URL( + 'https://lab.cdn.eyevinn.technology/sto-slate.mp4/manifest.m3u8' + ); + return new BarkerAssetManager(vodToLoop); + } + + newChannelManager(useDemuxedAudio: boolean): IChannelManager { + return new BarkerChannelManager( + process.env.BARKER_CHANNEL_NAME + ? process.env.BARKER_CHANNEL_NAME + : 'barker', + useDemuxedAudio + ); + } + + newStreamSwitchManager(): IStreamSwitchManager { + const liveStreamListUrl = process.env.BARKERLIST_URL + ? process.env.BARKERLIST_URL + : 'https://testcontent.eyevinn.technology/fast/barkertest.txt'; + const switchIntervalMs = process.env.SWITCH_INTERVAL_SEC + ? parseInt(process.env.SWITCH_INTERVAL_SEC) * 1000 + : undefined; + + return new BarkerStreamSwitchManager( + new URL(liveStreamListUrl), + switchIntervalMs + ); + } + } \ No newline at end of file diff --git a/src/plugins/plugin_demo.ts b/src/plugins/plugin_demo.ts new file mode 100644 index 0000000..d926047 --- /dev/null +++ b/src/plugins/plugin_demo.ts @@ -0,0 +1,109 @@ +import { + IAssetManager, + IChannelManager, + VodRequest, + VodResponse, + Channel, + ChannelProfile, + IStreamSwitchManager + } from 'eyevinn-channel-engine'; + import { BasePlugin, PluginInterface } from './interface'; + + const DEMO_NUM_CHANNELS = process.env.DEMO_NUM_CHANNELS + ? parseInt(process.env.DEMO_NUM_CHANNELS, 10) + : 12; + const DEFAULT_ASSETS = [ + 'https://maitv-vod.lab.eyevinn.technology/VINN.mp4/master.m3u8', + 'https://lab.cdn.eyevinn.technology/stswe19-industry-group-low-latency-hls.mp4/manifest.m3u8', + 'https://lab.cdn.eyevinn.technology/stswe19-global-but-local-ott-platform.mp4/manifest.m3u8', + 'https://lab.cdn.eyevinn.technology/stswe19-serverless-media-processing-at-netflix.mp4/manifest.m3u8', + 'https://lab.cdn.eyevinn.technology/NO_TIME_TO_DIE_short_Trailer_2021.mp4/manifest.m3u8', + 'https://lab.cdn.eyevinn.technology/THE_GRAND_BUDAPEST_HOTEL_Trailer_2014.mp4/manifest.m3u8', + 'https://lab.cdn.eyevinn.technology/stswe19-three-roads-to-jerusalem.mp4/manifest.m3u8', + 'https://lab.cdn.eyevinn.technology/wrc-jbi-arvija-finland-220126.mov/manifest.m3u8', + 'https://lab.cdn.eyevinn.technology/stswe19-challenge-to-preserver-creators-intent.mp4/manifest.m3u8', + 'https://lab.cdn.eyevinn.technology/BAAHUBALI_3_Trailer_2021.mp4/manifest.m3u8', + 'https://lab.cdn.eyevinn.technology/stswe22-talks-teaser-Z4-ehLIMe8.mp4/manifest.m3u8', + 'https://lab.cdn.eyevinn.technology/OWL_MVP_2021.mp4/manifest.m3u8', + 'https://lab.cdn.eyevinn.technology/stswe22-webrtc-flt5fm7bR3.mp4/manifest.m3u8', + 'https://lab.cdn.eyevinn.technology/wrc-jbi-sweden-220126-BP4uTVw_FV.mp4/manifest.m3u8', + 'https://lab.cdn.eyevinn.technology/f1-monaco-5l-jan8-5ULu9E6C_t.mov/manifest.m3u8', + 'https://maitv-vod.lab.eyevinn.technology/tearsofsteel_4k.mov/master.m3u8' + ]; + + class AssetManager implements IAssetManager { + private assets; + private pos; + constructor() { + this.assets = {}; + this.pos = {}; + for (let i = 0; i < DEMO_NUM_CHANNELS; i++) { + this.assets[i + 1] = []; + DEFAULT_ASSETS.forEach((asset) => { + this.assets[i + 1].push({ + id: i + 1, + title: `Asset ${i + 1}`, + uri: asset + }); + }); + this.pos[i + 1] = Math.floor(Math.random() * DEFAULT_ASSETS.length); + } + } + + async getNextVod(vodRequest: VodRequest): Promise { + const channelId = vodRequest.playlistId; + if (this.assets[channelId]) { + const vod = this.assets[channelId][this.pos[channelId]++]; + if (this.pos[channelId] > this.assets[channelId].length - 1) { + this.pos[channelId] = 0; + } + const vodResponse = { + id: vod.id, + title: vod.title, + uri: vod.uri + }; + return vodResponse; + } else { + throw new Error('Invalid channelId provided'); + } + } + } + + class ChannelManager implements IChannelManager { + getChannels(): Channel[] { + const channelList = []; + for (let i = 0; i < DEMO_NUM_CHANNELS; i++) { + channelList.push({ + id: `${i + 1}`, + profile: this._getProfile() + }); + } + return channelList; + } + + _getProfile(): ChannelProfile[] { + return [ + { bw: 6134000, codecs: 'avc1.4d001f,mp4a.40.2', resolution: [1024, 458] }, + { bw: 2323000, codecs: 'avc1.4d001f,mp4a.40.2', resolution: [640, 286] }, + { bw: 1313000, codecs: 'avc1.4d001f,mp4a.40.2', resolution: [480, 214] } + ]; + } + } + + export class DemoPlugin extends BasePlugin implements PluginInterface { + constructor() { + super('Demo'); + } + + newAssetManager(): IAssetManager { + return new AssetManager(); + } + + newChannelManager(): IChannelManager { + return new ChannelManager(); + } + + newStreamSwitchManager(): IStreamSwitchManager { + return undefined; + } + } \ No newline at end of file diff --git a/src/plugins/plugin_loop.ts b/src/plugins/plugin_loop.ts new file mode 100644 index 0000000..3544b85 --- /dev/null +++ b/src/plugins/plugin_loop.ts @@ -0,0 +1,88 @@ +import { + IAssetManager, + IChannelManager, + VodRequest, + VodResponse, + Channel, + ChannelProfile, + IStreamSwitchManager + } from 'eyevinn-channel-engine'; + + import { BasePlugin, PluginInterface } from './interface'; + import { + getDefaultChannelAudioProfile, + getDefaultChannelVideoProfile + } from './utils'; + + class LoopAssetManager implements IAssetManager { + private vodToLoop: URL; + + constructor(vodToLoop: URL) { + this.vodToLoop = vodToLoop; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getNextVod(vodRequest: VodRequest): Promise { + const vodResponse = { + id: 'loop', + title: 'VOD on Loop', + uri: this.vodToLoop.toString() + }; + return vodResponse; + } + } + + class LoopChannelManager implements IChannelManager { + private channelId: string; + private useDemuxedAudio: boolean; + + constructor(channelId: string, useDemuxedAudio: boolean) { + this.channelId = channelId; + this.useDemuxedAudio = useDemuxedAudio; + console.log( + `Loop channel available at /channels/${this.channelId}/master.m3u8` + ); + } + + getChannels(): Channel[] { + const channel: Channel = { + id: this.channelId, + profile: this._getProfile() + }; + if (this.useDemuxedAudio) { + channel.audioTracks = getDefaultChannelAudioProfile(); + } + const channelList = [channel]; + return channelList; + } + + _getProfile(): ChannelProfile[] { + return getDefaultChannelVideoProfile(); + } + } + + export class LoopPlugin extends BasePlugin implements PluginInterface { + constructor() { + super('Loop'); + } + + newAssetManager(): IAssetManager { + const vodToLoop = process.env.LOOP_VOD_URL + ? new URL(process.env.LOOP_VOD_URL) + : new URL( + 'https://lab.cdn.eyevinn.technology/eyevinn-reel-feb-2023-_2Y7i4eOAi.mp4/manifest.m3u8' + ); + return new LoopAssetManager(vodToLoop); + } + + newChannelManager(useDemuxedAudio: boolean): IChannelManager { + return new LoopChannelManager( + process.env.LOOP_CHANNEL_NAME ? process.env.LOOP_CHANNEL_NAME : 'loop', + useDemuxedAudio + ); + } + + newStreamSwitchManager(): IStreamSwitchManager { + return undefined; + } + } \ No newline at end of file