diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b4772c3..d056310 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,26 +15,22 @@ jobs: goreleaser: runs-on: ubuntu-latest steps: - - - name: Checkout + - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 0 - - - name: Set up Go + - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.16 - - - name: Cache Go modules + - name: Cache Go modules uses: actions/cache@v1 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - - - name: Run GoReleaser + - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 if: startsWith(github.ref, 'refs/tags/') with: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 98f6738..2da2e35 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -2,21 +2,20 @@ name: Mark stale issues and pull requests on: schedule: - - cron: "30 1 * * *" + - cron: '30 1 * * *' jobs: stale: - runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - - uses: actions/stale@v3 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-issue-message: 'Closing due to staleness' - stale-pr-message: 'Closing due to staleness' - stale-issue-label: 'no-issue-activity' - stale-pr-label: 'no-pr-activity' + - uses: actions/stale@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'Closing due to staleness' + stale-pr-message: 'Closing due to staleness' + stale-issue-label: 'no-issue-activity' + stale-pr-label: 'no-pr-activity' diff --git a/.goreleaser.yml b/.goreleaser.yml index 2af23e2..84fd03d 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,14 +1,12 @@ builds: - - - goos: + - goos: - windows - darwin - linux goarch: - amd64 archives: - - - replacements: + - replacements: amd64: 64-bit 386: 32-bit darwin: macos @@ -20,4 +18,4 @@ archives: - LICENSE* - LICENCE* - README* - - CHANGELOG* \ No newline at end of file + - CHANGELOG* diff --git a/FUNDING.yml b/FUNDING.yml index 4ea655e..efbac3d 100644 --- a/FUNDING.yml +++ b/FUNDING.yml @@ -1,5 +1,10 @@ # These are supported funding model platforms - +custom: + [ + 'https://www.twitch.tv/subs/updownleftdie', + 'https://www.guilded.gg/UpDownLeftDie/subscriptions?r=Vmy2GNwA', + 'https://www.paypal.me/jaredkotoff', + ] github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: updownleftdie open_collective: # Replace with a single Open Collective username @@ -9,5 +14,3 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username -custom: - ["https://www.paypal.me/jaredkotoff", "https://www.twitch.tv/subs/updownleftdie", "https://www.guilded.gg/UpDownLeftDie/subscriptions?r=Vmy2GNwA"] diff --git a/README.md b/README.md index a242ed6..1e11cb8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Play videos in random order! -*Perfect for BRB screens!* +_Perfect for BRB screens!_ ## [Download](https://github.com/UpDownLeftDie/obs-random-videos/releases/latest) @@ -23,32 +23,32 @@ Play videos in random order! #### Video -* mp4 -* m4v -* webm -* mpeg4 +- mp4 +- m4v +- webm +- mpeg4 #### Audio -* mp3 -* ogg -* aac +- mp3 +- ogg +- aac ## Notes -* Set `Refresh browser when scene becomes active` to randomize on each load -* Video resolutions should match your canvas aspect ratio -* **Autoplay only works in OBS!** - * If you want to test this in your browser: +- Set `Refresh browser when scene becomes active` +- Video resolutions should match your canvas aspect ratio +- **Autoplay only works in OBS!** + - If you want to test this in your browser: 1. Open the `obs-random-videos.html` with your browser 2. Right-click on the image and click "Show controls" 3. Hit the play button -* Pro tip: webm videos support transparency (convert mov to webm to save on file size) +- Pro tip: webm videos support transparency (convert mov to webm to save on file size) ## TODO -* Audio-only or video-only modes -* Option for cross-fading between videos -* Option to HTML background color - * Good for videos with weird aspect ratio - * Good for audio files +- Audio-only or video-only modes +- Option for cross-fading between videos +- Option to HTML background color + - Good for videos with weird aspect ratio + - Good for audio files diff --git a/go.mod b/go.mod index 68dc906..b382da5 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,14 @@ module github.com/UpDownLeftDie/obs-random-videos/v2 -go 1.16 +go 1.17 + +require github.com/sparkdemcisin81/promptui v1.0.0 require ( + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect + github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect github.com/lunixbochs/vtclean v1.0.0 // indirect - github.com/mattn/go-colorable v0.1.8 // indirect - github.com/sparkdemcisin81/promptui v1.0.0 - golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect + github.com/mattn/go-colorable v0.1.11 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect ) diff --git a/go.sum b/go.sum index aae9c7a..6636fe0 100644 --- a/go.sum +++ b/go.sum @@ -4,10 +4,12 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc h1:ZQrgZFsLzkw7o3CoDzsfBhx0bf/1rVBXrLy8dXKRe8o= +github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -15,17 +17,22 @@ github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQN github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.10/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/sparkdemcisin81/promptui v1.0.0 h1:GGK4vlIZP36dyM1PgKH0utVkk13kDUqOS5sy6XqeU6I= github.com/sparkdemcisin81/promptui v1.0.0/go.mod h1:RpZAFCsCFF7OVe+B1Pc4gEA1OAW0n7uQq43ck3rPpNk= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/js/body.js b/js/body.js index 3b97ca4..b33945c 100644 --- a/js/body.js +++ b/js/body.js @@ -1,17 +1,44 @@ -const player = document.getElementById("videoPlayer1"); -const player2 = document.getElementById("videoPlayer2"); -player.addEventListener("ended", () => playNext(player2, player), {passive: true}); -player2.addEventListener("ended", () => playNext(player, player2), {passive: true}); +// @ts-check +const player = /** @type {HTMLMediaElement} */ ( + document.getElementById('videoPlayer1') +); -/* Initial logic */ +const player2 = /** @type {HTMLMediaElement} */ ( + document.getElementById('videoPlayer2') +); +player.addEventListener( + 'ended', + () => { + if (!loopFirstVideo) { + progressPlaylistState(); + } + playNext(player2, player); + }, + { + passive: true, + }, +); +player2.addEventListener( + 'ended', + () => { + if (!loopFirstVideo) { + progressPlaylistState(); + } + playNext(player, player2); + }, + { + passive: true, + }, +); -const mp4Source = player.getElementsByClassName("mp4Source")[0]; -let video = `${mediaFolder}${mediaFiles[0]}`; -// check if we played this video last run -if (mediaFiles.length > 1 && localStorage.getItem("lastPlayed") === video) { - video = `${mediaFolder}${mediaFiles[1]}`; -} -mp4Source.setAttribute("src", video); +/***** Initial load *****/ + +const mp4Source = player.getElementsByClassName('mp4Source')[0]; +let video = getNextPlaylistItem(); +// have to move the state forward after getting the first video +progressPlaylistState(); + +mp4Source.setAttribute('src', video); player.load(); -playNext(player, player2); \ No newline at end of file +playNext(player, player2); diff --git a/js/main.js b/js/main.js index 17182ec..8206d6b 100644 --- a/js/main.js +++ b/js/main.js @@ -1,13 +1,20 @@ -const mediaFolder = "{{ .MediaFolder }}"; -const initMediaFiles = ["{{ StringsJoin .MediaFiles "\", \"" }}"]; -const transitionVideo = "{{ .TransitionVideo }}"; -const playOnlyOne = {{ .PlayOnlyOne }}; -const loopFirstVideo = {{ .LoopFirstVideo }}; +// @ts-check +const mediaFolder = /** @type {string} */ ("{{ .MediaFolder }}"); +const initMediaFiles = /** @type {string[]} */ (["{{ StringsJoin .MediaFiles "\", \"" }}"]); +const transitionVideo = /** @type {string} */("{{ .TransitionVideo }}"); +const playOnlyOne = /** @type {boolean} */ ({{ .PlayOnlyOne }}); +const loopFirstVideo = /** @type {boolean} */ ({{ .LoopFirstVideo }}); +const transitionVideoPath = /** @type {string} */ ( + `${mediaFolder}${transitionVideo}` +); -const mediaFiles = shuffleArr(initMediaFiles); -let count = 0; let isTransition = true; +/** + * shuffleArr takes in an array and returns a new array in random order + * @param {any[]} a any array to be shuffled + * @return {any[]} shuffled array + */ function shuffleArr(a) { var j, x, i; for (i = a.length - 1; i > 0; i--) { @@ -19,47 +26,134 @@ function shuffleArr(a) { return a; } +/** + * prependFolderToFiles simply adds a folder path to a list of files and returns + * the new array + * @param {string} folder folder path, must end with a trailing slash + * @param {string[]} files array of file names + * @returns {string[]} new array with full path to files + */ +function prependFolderToFiles(folder, files) { + return files.map((file) => `${folder}${file}`); +} + +/** + * storePlaylistState takes in a playlist and stores it into localstorage + * @param {string[]} state + * @returns {void} + */ +function storePlaylistState(state) { + localStorage.setItem('playlist', JSON.stringify(state)); +} + +/** + * getNewPlaylist creates a newly randomize list of files and stores it in + * localstorage + * @returns {string[]} a new playlist + */ +function getNewPlaylist() { + const playlist = prependFolderToFiles( + mediaFolder, + shuffleArr(initMediaFiles), + ); + storePlaylistState(playlist); + return playlist; +} + +/** + * getPlaylist will get the playlist state form localstorage or create a new one + * @returns {string[]} current playlist state + */ +function getPlaylist() { + let playlist = []; + try { + playlist = JSON.parse(localStorage.getItem('playlist')); + } catch { + console.log('playlist empty!'); + } + if (!playlist || playlist.length === 0) { + playlist = getNewPlaylist(); + } + return playlist; +} + +/** + * progressPlaylistState removes the last item from the playlist and stores the + * updated version in localstorage + * @returns {void} + */ +function progressPlaylistState() { + const playlist = getPlaylist(); + playlist.pop(); + storePlaylistState(playlist); +} + +/** + * getNextPlaylistItem returns the next item in the playlist unless it matches + * the last thing played then it moves that item to the end and returns the + * next item after that + * @returns {string} the next item in the playlist + */ +function getNextPlaylistItem() { + const playlist = getPlaylist(); + let mediaItem = playlist.pop(); + + // check if we played this mediaItem last run + console.log({ lastPlayed: localStorage.getItem('lastPlayed'), mediaItem }); + if (localStorage.getItem('lastPlayed') === mediaItem) { + // moves the repeated item to the end so its not skipped entirely + storePlaylistState([mediaItem].concat(playlist)); + mediaItem = getNextPlaylistItem(); + } + return mediaItem; +} + +/** + * playNext is the core function of this project and handles the loading and + * playing of the alternating video players + * @param {HTMLMediaElement} player currently playing video player + * @param {HTMLMediaElement} nextPlayer the next video player to be played + * @returns {void} + */ function playNext(player, nextPlayer) { - const nextMp4Source = nextPlayer.getElementsByClassName("mp4Source")[0]; const currentMp4Source = player.getElementsByClassName('mp4Source')[0]; - if(!loopFirstVideo) { - if (!transitionVideo || !isTransition) { - count++; - if (count > mediaFiles.length - 1) count = 0; - } + const nextMp4Source = nextPlayer.getElementsByClassName('mp4Source')[0]; + const currentVideo = currentMp4Source.getAttribute('src'); + if (currentVideo !== transitionVideoPath) { + localStorage.setItem('lastPlayed', currentVideo); + } + + let video = localStorage.getItem('lastPlayed'); + if (!loopFirstVideo && (!transitionVideo || !isTransition)) { + video = getNextPlaylistItem(); + console.log(`next video: ${video}`); } + // TODO: we can use this opacity to crossfade between mediaFiles + player.style['z-index'] = 1; + player.style['opacity'] = '1'; + nextPlayer.style['z-index'] = 0; + nextPlayer.style['opacity'] = '0'; + + if (transitionVideo && transitionVideo !== '' && isTransition) { + video = transitionVideoPath; + isTransition = false; + } else { + isTransition = true; + } + nextMp4Source.setAttribute('src', video); + nextPlayer.load(); + nextPlayer.pause(); + if (playOnlyOne) { - if (count > 1) { - // Remove video after playing once + // Remove videos after playing once + player.onended = () => { currentMp4Source.removeAttribute('src'); nextMp4Source.removeAttribute('src'); player.load(); nextPlayer.load(); - } - } else { - // TODO: we can use this opacity to crossfade between mediaFiles - player.style["z-index"] = 1; - player.style["opacity"] = 1; - nextPlayer.style["z-index"] = 0; - nextPlayer.style["opacity"] = 0; - let video = `${mediaFolder}${mediaFiles[count]}`; - - const transitionVideoPath = `${mediaFolder}${transitionVideo}` - if (transitionVideo && transitionVideo !== "" && isTransition) { - video = transitionVideoPath; - isTransition = false; - } else { - isTransition = true; - } - nextMp4Source.setAttribute("src", video); - nextPlayer.load(); - nextPlayer.pause(); - - const currentVideo = currentMp4Source.getAttribute("src"); - if (currentVideo !== transitionVideoPath) { - localStorage.setItem("lastPlayed", currentVideo); - } - player.play(); + }; } -} \ No newline at end of file + + player.play(); +}