Skip to content

Commit

Permalink
Merge pull request #4 from missmatsuko/v2
Browse files Browse the repository at this point in the history
v2
  • Loading branch information
missmatsuko authored Dec 6, 2018
2 parents a1bd45d + 909922d commit eb999cd
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 207 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# dist
dist.zip
26 changes: 4 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,10 @@
# asl-tab
Chrome extension that loads a random ASL instructional video in new tabs. Content from [ASLU Signs YouTube Channel](https://www.youtube.com/channel/UCZy9xs6Tn9vWqN_5l0EEIZA).
Loads a random, short American Sign Language (ASL) instructional video in new tabs. Videos from [Dr. Bill Vicars of LifePrint/ASLU](https://www.youtube.com/channel/UCZy9xs6Tn9vWqN_5l0EEIZA).

### Download the ASL Tab browser extension:
## Download the browser extension:
- [Chrome](https://chrome.google.com/webstore/detail/asl-tab/bjiakmejoofpfclmopcfpkopmamecnkd)
- [Firefox](https://addons.mozilla.org/en-US/firefox/addon/asl-tab/)

### Thanks
## Thanks
- Dr. Bill Vicars of ASLU (LifePrint) for content - [YouTube](https://www.youtube.com/user/billvicars), [Website](http://lifeprint.com/)
- [Bart Nagel (@tremby)](https://github.com/tremby) for code helps

### Roadmap
- [x] Publish the Chrome extension
- [ ] Port extension to other browsers:
- [x] FireFox
- [ ] Edge (maybe)
- [ ] Add server-side component:
- [ ] Hide API key
- [ ] Reduce client-side YouTube Data API calls
- [ ] Stop using the playlist position undocumented workaround (hack)
- [ ] Store video IDs and titles for valid videos (short duration, available on YouTube)
- [ ] Update video IDs and titles periodically (new videos) and when they're consistently unreachable (deleted)
- [ ] User settings (maybe)
- [ ] Max video duration
- [ ] Min time until video can repeat
- [ ] Use own API key
- [ ] (Don't) loop video
- [ ] (Don't) autoplay video
- Bart Nagel (@tremby) for code helps - [GitHub](https://github.com/tremby), [Website](https://bartnagel.ca/)
4 changes: 2 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "ASL Tab",
"version": "1.1.0",
"version": "2.0.0",
"description": "Loads a random, short American Sign Language (ASL) instructional video in new tabs. Videos from Dr. Bill Vicars of LifePrint/ASLU.",
"icons": {
"16": "icons/icon16.png",
Expand All @@ -15,5 +15,5 @@
"https://www.youtube.com/*",
"https://www.googleapis.com/*"
],
"content_security_policy": "script-src 'self' https://www.youtube.com https://s.ytimg.com https://www.googleapis.com; object-src 'self'"
"content_security_policy": "script-src 'self' https://www.youtube.com https://www.googleapis.com; object-src 'self'"
}
2 changes: 1 addition & 1 deletion newTab.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ main {
padding: 1em;
}

#iframe-wrapper {
#iframe-container {
position: relative;
width: 100%;
height: 0;
Expand Down
14 changes: 2 additions & 12 deletions newTab.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,13 @@

<main>
<div class="content-wrapper">
<h1 id="pageHeading">loading...</h1>
<h1 id="heading">loading...</h1>

<div id="iframe-wrapper">
<iframe
width="560"
height="315"
frameborder="0"
src="https://www.youtube.com/embed/playlist?list=UUZy9xs6Tn9vWqN_5l0EEIZA&rel=0&mute=1&enablejsapi=1"
allow="fullscreen"
id="iframe"
>
</iframe>
<div id="iframe-container">
</div>
</div>
</main>

<script src="https://www.youtube.com/iframe_api"></script>
<script src="/newTab.js"></script>
</body>
</html>
270 changes: 100 additions & 170 deletions newTab.js
Original file line number Diff line number Diff line change
@@ -1,202 +1,132 @@
// Global Variables
const YOUTUBE_API_KEY = 'AIzaSyCdZdNVQ6XbhA1AQQ1ZDK1qZCXitP6RPOA';
const PLAYLIST_ID = 'UUZy9xs6Tn9vWqN_5l0EEIZA';
const MAX_VIDEO_DURATION = 10; // Max duration of videos to play, in seconds
const STORED_NUM_VIDEOS_DURATION = 7 * 24 * 60 * 60; // Duration to store number of videos in playlist; 7 days, in seconds
const STORED_VIDEO_POSITION_DURATION = 1 * 24 * 60 * 60; // Duration to store video position; 1 day, in seconds
const IFRAME_ID = 'iframe';
const NUM_VIDEOS_IN_PLAYLIST_EMBED = 100; // Number of videos that are supposed to be in an embedded playlist
const MAX_ROLL_COUNT = 50; // Max times to reroll for random video
const REPEAT_VIDEO_DURATION = 24 * 60 * 60 * 1000; // Min (ideal) duration before repeating a video, in milliseconds
const VIDEOS_DATA_ENDPOINT = 'https://s3.ca-central-1.amazonaws.com/asl-tab-api-data/data.json';
const DEFAULT_IFRAME_SRC = 'https://www.youtube.com/embed/playlist?list=UUZy9xs6Tn9vWqN_5l0EEIZA&rel=0&mute=1'; // ASL signs playlist
const DEFAULT_HEADING_TEXT = 'ASL Tab';

let player = null;
let numVideos = NUM_VIDEOS_IN_PLAYLIST_EMBED; // Number of videos available to randomize from
let videosData = [];

// DOM Elements
let iFrameElement = document.getElementById(IFRAME_ID);
const iFrameWrapperElement = document.getElementById('iframe-wrapper');
const pageHeading = document.getElementById('pageHeading');

/*
Given a base URL and an object of query parameters as key/value pairs,
returns a composed query parameter URL
*/
const composeEndpointUrl = function(baseUrl, queryParams) {
const queryParamsArray = Object.entries(queryParams).map(([key, value]) => {
return `${key}=${encodeURIComponent(value)}`;
});
return `${baseUrl}?${queryParamsArray.join('&')}`;
const iframeContainerEl = document.getElementById('iframe-container');
const headingEl = document.getElementById('heading');

// Gets a random number in a given range
const getRandomNumber = function(min = 0, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

// Updates heading element's text
const updateHeadingText = function(text) {
headingEl.textContent = text;
}

// Returns true if the video is valid
const checkVideo = function() {
const parsedIframeUrl = new URL(iFrameElement.src);
const iframeVideoIndex = parseInt(parsedIframeUrl.searchParams.get('index'), 10);
const playerVideoIndex = player.getPlaylistIndex();
const videoDuration = player.getDuration(); // seconds
// Inserts iframe into the iframeContainerEl
const insertIframe = function(src) {
// Create an iFrame
const iframe = document.createElement('iframe');
iframe.frameBorder = 0;
iframe.allow = 'fullscreen';
iframe.src = src;

if (videoDuration > MAX_VIDEO_DURATION) {
// Append it inside iframeContainerEl
iframeContainerEl.appendChild(iframe);
}

// Replaces iframe src so it stops autoplaying
// Workaround due to not being able to use YouTube iframe player API for Firefox
const stopAutoplay = function() {
const iframe = iframeContainerEl.querySelector('iframe');
const iframeSrc = iframe.src;
const newIframeSrc = iframeSrc.replace('&autoplay=1', '');
iframe.remove();
insertIframe(newIframeSrc);
}

// Gets fresh videos data, returns false otherwise
const getVideosData = async function() {
const response = await fetch(VIDEOS_DATA_ENDPOINT);
return response.ok ? response.json() : false;
}

// Check if video can be played
const checkVideosItem = function(videosItem) {

// Check if duration is acceptable
if (videosItem.duration > MAX_VIDEO_DURATION) {
return false;
}

// If the videoIndex doesn't match randomVideoIndex, try a different video within 100 (thinking it means the getting a video outside of NUM_VIDEOS_IN_PLAYLIST_EMBED hack stopped working)
if (iframeVideoIndex !== playerVideoIndex) {
numVideos = NUM_VIDEOS_IN_PLAYLIST_EMBED;
// Check if video has been recently played
const videosItemLastPlayed = localStorage.getItem(videosItem.id);
if (videosItemLastPlayed && Date.now() - videosItemLastPlayed < REPEAT_VIDEO_DURATION) {
return false;
}

return true;
}

// Get a random video position (1-indexed)
const getRandomVideoPosition = function() {
const randomVideoPosition = Math.floor(Math.random() * numVideos);
const storedVideoPositionDate = localStorage.getItem(randomVideoPosition);
const storedVideoPositionExpired = Date.now() - storedVideoPositionDate > STORED_VIDEO_POSITION_DURATION;
// Gets a random item from videos data with playing time less than MAX_VIDEO_DURATION
const getVideosItem = function() {
const videosDataLength = videosData.length;
let videosItem = undefined;
let rollCount = 0;

// Re-roll once if the video position is not expired
if (!storedVideoPositionExpired) {
return Math.floor(Math.random() * numVideos);
} else {
localStorage.setItem(randomVideoPosition, Date.now());
return randomVideoPosition;
}
}
while (true) {
const videoIndex = getRandomNumber(0, videosDataLength - 1);
videosItem = videosData[videoIndex];
rollCount++;

/*
Plays a random video
NOTE: There is a method in the IFrame Player API to set the playlist position,
but that doesn't allow setting a position beyond NUM_VIDEOS_IN_PLAYLIST_EMBED
REF: https://developers.google.com/youtube/iframe_api_reference#playVideoAt
*/
const playRandomVideo = async function() {
let randomVideoIndex = getRandomVideoPosition() - 1;
if (rollCount > MAX_ROLL_COUNT || checkVideosItem(videosItem)) {
break;
}
}

const iFrameSrc = iFrameElement.src;
localStorage.setItem(videosItem.id, Date.now());
return videosItem;
}

if (player) {
player.destroy();
// Main program
const init = async function() {
// Set fallback iframe src and heading text
let newIframeSrc = DEFAULT_IFRAME_SRC;
let newHeadingText = DEFAULT_HEADING_TEXT;

// Recreate IFrame
iFrameElement = document.createElement('iframe');
// Get videos data
videosData = await getVideosData();

// Add default attributes
iFrameElement.src = iFrameSrc;
iFrameElement.id = IFRAME_ID;
iFrameElement.frameBorder = 0;
iFrameElement.allow = 'fullscreen';
if (videosData.length) {
// Embed a random video from the entire playlist
const videosItem = getVideosItem();
const videoId = videosItem.id;
const videoTitle = videosItem.title;

iFrameWrapperElement.appendChild(iFrameElement);
// NOTE: playlist param set to video ID to enable looping as looping is only enabled on playlist embeds and this magically makes it into a valid playlist
newIframeSrc = `https://www.youtube.com/embed/${ videoId }?&playlist=${ videoId }&loop=1&autoplay=1&mute=1`;
newHeadingText = videoTitle;
} else {
// Embed a random video from the latest 100 items of the playlist
// 100 is the actual amount of videos expected in the playlist
const videoIndex = getRandomNumber(0, 100);
newIframeSrc = `${ DEFAULT_IFRAME_SRC }&index=${ videoIndex }&autoplay=1&mute=1`;
}

// Set index query parameter of iFrameElement
const parsedIframeUrl = new URL(iFrameSrc);
parsedIframeUrl.searchParams.set('index', randomVideoIndex);
iFrameElement.src = parsedIframeUrl.href;
// Insert the iFrame into page
insertIframe(newIframeSrc);

// Wait for iframe src to update
await new Promise((resolve, reject) => {
setTimeout(resolve);
});
// Update heading text
updateHeadingText(newHeadingText);

// Ensure YouTube IFrame API is ready
await youtubeIframeApiPromise;

// Create player instance from YouTube IFrame embed
return new Promise((resolve, reject) => {
player = new YT.Player(IFRAME_ID, {
events: {
onReady: async () => {
// Check if video is good
if (checkVideo()) {
// Get info of currently playing video
const videoUrl = player.getVideoUrl();
const parsedVideoUrl = new URL(videoUrl);
const videoId = parsedVideoUrl.searchParams.get('v');

// Change playlist embed into a single video embed
player.loadVideoById(videoId);

player.playVideo();

// Update title and heading text
fetch(
composeEndpointUrl(
'https://www.googleapis.com/youtube/v3/videos',
{
'key': YOUTUBE_API_KEY,
'part': 'snippet',
'id': videoId,
}
)
)
.then(response => response.json())
.then((data) => {
if (data.items.length) {
const videoTitle = data.items[0].snippet.title;
document.title += ` | ${ videoTitle }`;
pageHeading.textContent = videoTitle;
}
})
.catch(error => console.error('Error:', error));
} else {
// Randomize again
await playRandomVideo();
}

resolve();
},
onStateChange: (event) => {
// Fake loop the video
// NOTE: Playlists can be looped via YouTube API, but single videos cannot
if (event.data === YT.PlayerState.ENDED) {
player.playVideo();
}
}
}
});
// Stop playing video when document is hidden, or when 1 minute passes
window.addEventListener('visibilitychange', (e) => {
if (document.hidden) {
stopAutoplay();
}
});

setTimeout(stopAutoplay, 60 * 1000);
}

// Create promise for onYouTubeIframeAPIReady
const youtubeIframeApiPromise = new Promise((resolve, reject) => {
window.onYouTubeIframeAPIReady = function() {
resolve();
delete window.onYouTubeIframeAPIReady;
}
});

const updateNumVideos = function() {
const storedNumVideos = localStorage.getItem('NUM_VIDEOS');
const storedNumVideosDate = localStorage.getItem('NUM_VIDEOS_DATE');
const storedNumVideosExpired = Date.now() - storedNumVideosDate > STORED_NUM_VIDEOS_DURATION;

if (!storedNumVideosExpired && storedNumVideos >= NUM_VIDEOS_IN_PLAYLIST_EMBED) {
// Use the value from LocalStorage
numVideos = storedNumVideos;
} else {
// Use the actual number of videos in playlist
fetch(
composeEndpointUrl(
'https://www.googleapis.com/youtube/v3/playlists',
{
'key': YOUTUBE_API_KEY,
'id': PLAYLIST_ID,
'part': 'contentDetails',
}
)
)
.then(response => response.json())
.then((data) => {
if (data.items.length) {
numVideos = data.items[0].contentDetails.itemCount;
localStorage.setItem('NUM_VIDEOS', numVideos);
localStorage.setItem('NUM_VIDEOS_DATE', Date.now());
}
})
.catch(error => console.error('Error:', error));
}
}

// Main JS
updateNumVideos();
playRandomVideo();
// Run the program
init();
Loading

0 comments on commit eb999cd

Please sign in to comment.