Save Video Progress (#179)

* Added cast integration docs.

* Changed header sizes.

* Added more space above Requirements

* Added cast integration docs.

* Removed separate cast integration docs

* Further indented quote from Google

* Switch to HTML based video position.

* Ground work for API changes and video progress

* Added onpause attribute to video.

* Added save video progress feature.

* Added API check for subtitle status.

* Switch method to DELETE if position is 0

* Added `createVideoTag()` function

* Added `InsertVideoTag()` function

* Switch to JS generated video tag, add on page load

* Removed extra data from DELETE request

* Removed unused code

* Reduced duplicate code

* Cleanup & groundwork cast pull metadata from API

* Minor bug fix

* Fix saving video progress on player close.

* Only send video progress when unwatched

* Cleanup

* Added `getURL()` function

* Cast use API & save progress/mark as watched

* Added cast progress checks

* Changed thresholds for marking videos as watched

* Added `watchedThreshold()` function
This commit is contained in:
Nathan DeTar 2022-02-23 18:36:31 -08:00 committed by GitHub
parent 70506ad8f6
commit 4812b8da55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 216 additions and 88 deletions

View File

@ -2,17 +2,7 @@
{% block content %} {% block content %}
{% load static %} {% load static %}
{% load humanize %} {% load humanize %}
<div class="video-main"> <div class="video-main"></div>
<video poster="/cache/{{ video.vid_thumb_url }}" controls preload="false" width="100%" playsinline
ontimeupdate="onVideoProgress('{{ video.youtube_id }}')" onloadedmetadata="setVideoProgress(0)" id="video-item">
<source src="/media/{{ video.media_url }}" type="video/mp4" id="video-source">
{% if video.subtitles %}
{% for subtitle in video.subtitles %}
<track label="{{subtitle.name}}" kind="subtitles" srclang="{{subtitle.lang}}" src="/media/{{subtitle.media_url}}">
{% endfor %}
{% endif %}
</video>
</div>
<div class="boxed-content"> <div class="boxed-content">
<div class="title-bar"> <div class="title-bar">
{% if cast %} {% if cast %}
@ -122,4 +112,7 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</div> </div>
<script>
window.onload = insertVideoTag('{{ video.youtube_id }}');
</script>
{% endblock content %} {% endblock content %}

View File

@ -13,6 +13,16 @@ function initializeCastApi() {
castConnectionChange(player) castConnectionChange(player)
} }
); );
playerController.addEventListener(
cast.framework.RemotePlayerEventType.CURRENT_TIME_CHANGED, function() {
castVideoProgress(player)
}
);
playerController.addEventListener(
cast.framework.RemotePlayerEventType.IS_PAUSED_CHANGED, function() {
castVideoPaused(player)
}
);
} }
@ -26,32 +36,64 @@ function castConnectionChange(player) {
} }
} }
function castVideoProgress(player) {
var videoId = getVideoPlayerVideoId();
if (player.mediaInfo.contentId.includes(videoId)) {
var currentTime = player.currentTime;
var duration = player.duration;
if ((currentTime % 10) <= 1.0 && currentTime != 0 && duration != 0) { // Check progress every 10 seconds or else progress is checked a few times a second
postVideoProgress(videoId, currentTime);
if (!getVideoPlayerWatchStatus()) { // Check if video is already marked as watched
if (watchedThreshold(currentTime, duration)) {
isWatched(videoId);
}
}
}
}
}
function castVideoPaused(player) {
var videoId = getVideoPlayerVideoId();
var currentTime = player.currentTime;
var duration = player.duration;
if (player.mediaInfo != null) {
if (player.mediaInfo.contentId.includes(videoId)) {
if (currentTime != 0 && duration != 0) {
postVideoProgress(videoId, currentTime);
}
}
}
}
function castStart() { function castStart() {
var castSession = cast.framework.CastContext.getInstance().getCurrentSession(); var castSession = cast.framework.CastContext.getInstance().getCurrentSession();
// Check if there is already media playing on the cast target to prevent recasting on page reload or switching to another video page // Check if there is already media playing on the cast target to prevent recasting on page reload or switching to another video page
if (!castSession.getMediaSession()) { if (!castSession.getMediaSession()) {
contentId = document.getElementById("video-source").src; // Get video URL var videoId = getVideoPlayerVideoId();
contentTitle = document.getElementById('video-title').innerHTML; // Get video title var videoData = getVideoData(videoId);
contentImage = document.getElementById("video-item").poster; // Get video thumbnail URL var contentId = getURL() + videoData.data.media_url;
var contentTitle = videoData.data.title;
var contentImage = getURL() + videoData.data.vid_thumb_url;
contentType = 'video/mp4'; // Set content type, only videos right now so it is hard coded contentType = 'video/mp4'; // Set content type, only videos right now so it is hard coded
contentCurrentTime = document.getElementById("video-item").currentTime; // Get video's current position contentCurrentTime = getVideoPlayerCurrentTime(); // Get video's current position
contentActiveSubtitle = []; contentActiveSubtitle = [];
// Check if a subtitle is turned on. // Check if a subtitle is turned on.
for (var i = 0; i < document.getElementById("video-item").textTracks.length; i++) { for (var i = 0; i < getVideoPlayer().textTracks.length; i++) {
if (document.getElementById("video-item").textTracks[i].mode == "showing") { if (getVideoPlayer().textTracks[i].mode == "showing") {
contentActiveSubtitle =[i + 1]; contentActiveSubtitle =[i + 1];
} }
} }
contentSubtitles = []; contentSubtitles = [];
for (var i = 0; i < document.getElementById("video-item").children.length; i++) { var videoSubtitles = videoData.data.subtitles; // Array of subtitles
if (document.getElementById("video-item").children[i].tagName == "TRACK") { if (typeof(videoSubtitles) != 'undefined' && videoData.config.downloads.subtitle) {
for (var i = 0; i < videoSubtitles.length; i++) {
subtitle = new chrome.cast.media.Track(i, chrome.cast.media.TrackType.TEXT); subtitle = new chrome.cast.media.Track(i, chrome.cast.media.TrackType.TEXT);
subtitle.trackContentId = document.getElementById("video-item").children[i].src; subtitle.trackContentId = videoSubtitles[i].media_url;
subtitle.trackContentType = 'text/vtt'; subtitle.trackContentType = 'text/vtt';
subtitle.subtype = chrome.cast.media.TextTrackType.SUBTITLES; subtitle.subtype = chrome.cast.media.TextTrackType.SUBTITLES;
subtitle.name = document.getElementById("video-item").children[i].label; subtitle.name = videoSubtitles[i].name;
subtitle.language = document.getElementById("video-item").children[i].srclang; subtitle.language = videoSubtitles[i].lang;
subtitle.customData = null; subtitle.customData = null;
contentSubtitles.push(subtitle); contentSubtitles.push(subtitle);
} }
@ -91,7 +133,7 @@ function shiftCurrentTime(contentCurrentTime) { // Shift media back 3 seconds to
function castSuccessful() { function castSuccessful() {
// console.log('Cast Successful.'); // console.log('Cast Successful.');
document.getElementById("video-item").pause(); // Pause browser video on successful cast getVideoPlayer().pause(); // Pause browser video on successful cast
} }
function castFailed(errorCode) { function castFailed(errorCode) {

View File

@ -9,7 +9,7 @@ function sortChange(sortValue) {
} }
function isWatched(youtube_id) { function isWatched(youtube_id) {
// sendVideoProgress(youtube_id, 0); // Reset video progress on watched; postVideoProgress(youtube_id, 0); // Reset video progress on watched;
var payload = JSON.stringify({'watched': youtube_id}); var payload = JSON.stringify({'watched': youtube_id});
sendPost(payload); sendPost(payload);
var seenIcon = document.createElement('img'); var seenIcon = document.createElement('img');
@ -34,7 +34,7 @@ function isWatchedButton(button) {
} }
function isUnwatched(youtube_id) { function isUnwatched(youtube_id) {
// sendVideoProgress(youtube_id, 0); // Reset video progress on unwatched; postVideoProgress(youtube_id, 0); // Reset video progress on unwatched;
var payload = JSON.stringify({'un_watched': youtube_id}); var payload = JSON.stringify({'un_watched': youtube_id});
sendPost(payload); sendPost(payload);
var unseenIcon = document.createElement('img'); var unseenIcon = document.createElement('img');
@ -298,20 +298,12 @@ function cancelDelete() {
function createPlayer(button) { function createPlayer(button) {
var videoId = button.getAttribute('data-id'); var videoId = button.getAttribute('data-id');
var videoData = getVideoData(videoId); var videoData = getVideoData(videoId);
var videoUrl = videoData.media_url; var videoName = videoData.data.title;
var videoThumbUrl = videoData.vid_thumb_url;
var videoName = videoData.title;
var subtitles = ''; var videoTag = createVideoTag(videoId);
var videoSubtitles = videoData.subtitles; // Array of subtitles
if (typeof(videoSubtitles) != 'undefined') {
for (var i = 0; i < videoSubtitles.length; i++) {
subtitles += `<track label="${videoSubtitles[i].name}" kind="subtitles" srclang="${videoSubtitles[i].lang}" src="${videoSubtitles[i].media_url}">`;
}
}
var playlist = ''; var playlist = '';
var videoPlaylists = videoData.playlist; // Array of playlists the video is in var videoPlaylists = videoData.data.playlist; // Array of playlists the video is in
if (typeof(videoPlaylists) != 'undefined') { if (typeof(videoPlaylists) != 'undefined') {
var subbedPlaylists = getSubbedPlaylists(videoPlaylists); // Array of playlist the video is in that are subscribed var subbedPlaylists = getSubbedPlaylists(videoPlaylists); // Array of playlist the video is in that are subscribed
if (subbedPlaylists.length != 0) { if (subbedPlaylists.length != 0) {
@ -322,24 +314,22 @@ function createPlayer(button) {
} }
} }
var videoProgress = videoData.player.progress; // Groundwork for saving video position, change once progress variable is added to API var videoViews = formatNumbers(videoData.data.stats.view_count);
var videoViews = formatNumbers(videoData.stats.view_count);
var channelId = videoData.channel.channel_id; var channelId = videoData.data.channel.channel_id;
var channelName = videoData.channel.channel_name; var channelName = videoData.data.channel.channel_name;
removePlayer(); removePlayer();
document.getElementById(videoId).outerHTML = ''; // Remove watch indicator from video info document.getElementById(videoId).outerHTML = ''; // Remove watch indicator from video info
// If cast integration is enabled create cast button // If cast integration is enabled create cast button
var castButton = ``; var castButton = '';
var castScript = document.getElementById('cast-script'); if (videoData.config.application.enable_cast) {
if (typeof(castScript) != 'undefined' && castScript != null) {
var castButton = `<google-cast-launcher id="castbutton"></google-cast-launcher>`; var castButton = `<google-cast-launcher id="castbutton"></google-cast-launcher>`;
} }
// Watched indicator // Watched indicator
if (videoData.player.watched) { if (videoData.data.player.watched) {
var playerState = "seen"; var playerState = "seen";
var watchedFunction = "Unwatched"; var watchedFunction = "Unwatched";
} else { } else {
@ -348,22 +338,19 @@ function createPlayer(button) {
} }
var playerStats = `<div class="thumb-icon player-stats"><img src="/static/img/icon-eye.svg" alt="views icon"><span>${videoViews}</span>`; var playerStats = `<div class="thumb-icon player-stats"><img src="/static/img/icon-eye.svg" alt="views icon"><span>${videoViews}</span>`;
if (videoData.stats.like_count) { if (videoData.data.stats.like_count) {
var likes = formatNumbers(videoData.stats.like_count); var likes = formatNumbers(videoData.data.stats.like_count);
playerStats += `<span>|</span><img src="/static/img/icon-thumb.svg" alt="thumbs-up"><span>${likes}</span>`; playerStats += `<span>|</span><img src="/static/img/icon-thumb.svg" alt="thumbs-up"><span>${likes}</span>`;
} }
if (videoData.stats.dislike_count) { if (videoData.data.stats.dislike_count && videoData.config.downloads.integrate_ryd) {
var dislikes = formatNumbers(videoData.stats.dislike_count); var dislikes = formatNumbers(videoData.data.stats.dislike_count);
playerStats += `<span>|</span><img class="dislike" src="/static/img/icon-thumb.svg" alt="thumbs-down"><span>${dislikes}</span>`; playerStats += `<span>|</span><img class="dislike" src="/static/img/icon-thumb.svg" alt="thumbs-down"><span>${dislikes}</span>`;
} }
playerStats += "</div>"; playerStats += "</div>";
const markup = ` const markup = `
<div class="video-player" data-id="${videoId}"> <div class="video-player" data-id="${videoId}">
<video poster="${videoThumbUrl}" ontimeupdate="onVideoProgress('${videoId}')" controls autoplay width="100%" playsinline id="video-item"> ${videoTag}
<source src="${videoUrl}#t=${videoProgress}" type="video/mp4" id="video-source">
${subtitles}
</video>
<div class="player-title boxed-content"> <div class="player-title boxed-content">
<img class="close-button" src="/static/img/icon-close.svg" alt="close-icon" data="${videoId}" onclick="removePlayer()" title="Close player"> <img class="close-button" src="/static/img/icon-close.svg" alt="close-icon" data="${videoId}" onclick="removePlayer()" title="Close player">
<img src="/static/img/icon-${playerState}.svg" alt="${playerState}-icon" id="${videoId}" onclick="is${watchedFunction}(this.id)" class="${playerState}-icon" title="Mark as ${watchedFunction}"> <img src="/static/img/icon-${playerState}.svg" alt="${playerState}-icon" id="${videoId}" onclick="is${watchedFunction}(this.id)" class="${playerState}-icon" title="Mark as ${watchedFunction}">
@ -377,47 +364,121 @@ function createPlayer(button) {
</div> </div>
</div> </div>
`; `;
const divPlayer = document.getElementById("player"); const divPlayer = document.getElementById("player");
divPlayer.innerHTML = markup; divPlayer.innerHTML = markup;
} }
// Set video progress in seconds // Add video tag to video page when passed a video id, function loaded on page load `video.html (115-117)`
function setVideoProgress(videoProgress) { function insertVideoTag(videoId) {
if (isNaN(videoProgress)) { var videoTag = createVideoTag(videoId);
videoProgress = 0; var videoMain = document.getElementsByClassName("video-main");
} videoMain[0].innerHTML = videoTag;
var videoElement = document.getElementById("video-item");
videoElement.currentTime = videoProgress;
} }
// Runs on video playback, marks video as watched if video gets to 90% or higher, WIP sends position to api // Generates a video tag with subtitles when passed a video id.
function onVideoProgress(videoId) { function createVideoTag(videoId) {
var videoData = getVideoData(videoId);
var videoProgress = getVideoProgress(videoId).position;
var videoUrl = videoData.data.media_url;
var videoThumbUrl = videoData.data.vid_thumb_url;
var subtitles = '';
var videoSubtitles = videoData.data.subtitles; // Array of subtitles
if (typeof(videoSubtitles) != 'undefined' && videoData.config.downloads.subtitle) {
for (var i = 0; i < videoSubtitles.length; i++) {
subtitles += `<track label="${videoSubtitles[i].name}" kind="subtitles" srclang="${videoSubtitles[i].lang}" src="${videoSubtitles[i].media_url}">`;
}
}
var videoTag = `
<video poster="${videoThumbUrl}" ontimeupdate="onVideoProgress()" onpause="onVideoPause()" controls autoplay width="100%" playsinline id="video-item">
<source src="${videoUrl}#t=${videoProgress}" type="video/mp4" id="video-source" videoid="${videoId}">
${subtitles}
</video>
`;
return videoTag;
}
// Gets video tag
function getVideoPlayer() {
var videoElement = document.getElementById("video-item"); var videoElement = document.getElementById("video-item");
return videoElement;
}
// Gets the video source tag
function getVideoPlayerVideoSource() {
var videoPlayerVideoSource = document.getElementById("video-source");
return videoPlayerVideoSource;
}
// Gets the current progress of the video currently in the player
function getVideoPlayerCurrentTime() {
var videoElement = getVideoPlayer();
if (videoElement != null) { if (videoElement != null) {
if ((videoElement.currentTime % 10).toFixed(1) <= 0.2) { // Check progress every 10 seconds or else progress is checked a few times a second return videoElement.currentTime;
// sendVideoProgress(videoId, videoElement.currentTime); // Groundwork for saving video position }
if (((videoElement.currentTime / videoElement.duration) >= 0.90) && document.getElementById(videoId).className == "unseen-icon") { }
// Gets the video id of the video currently in the player
function getVideoPlayerVideoId() {
var videoPlayerVideoSource = getVideoPlayerVideoSource();
if (videoPlayerVideoSource != null) {
return videoPlayerVideoSource.getAttribute("videoid");
}
}
// Gets the duration of the video currently in the player
function getVideoPlayerDuration() {
var videoElement = getVideoPlayer();
if (videoElement != null) {
return videoElement.duration;
}
}
// Gets current watch status of video based on watch button
function getVideoPlayerWatchStatus() {
var videoId = getVideoPlayerVideoId();
var watched = false;
if(document.getElementById(videoId).className != "unseen-icon") {
watched = true;
}
return watched;
}
// Runs on video playback, marks video as watched if video gets to 90% or higher, sends position to api
function onVideoProgress() {
var videoId = getVideoPlayerVideoId();
var currentTime = getVideoPlayerCurrentTime();
var duration = getVideoPlayerDuration();
if ((currentTime % 10).toFixed(1) <= 0.2) { // Check progress every 10 seconds or else progress is checked a few times a second
postVideoProgress(videoId, currentTime);
if (!getVideoPlayerWatchStatus()) { // Check if video is already marked as watched
if (watchedThreshold(currentTime, duration)) {
isWatched(videoId); isWatched(videoId);
} }
} }
} }
} }
// Groundwork for saving video position function watchedThreshold(currentTime, duration) {
function sendVideoProgress(videoId, videoProgress) { var watched = false;
var apiEndpoint = "/api/video/"; if (duration <= 1800){ // If video is less than 30 min
if (isNaN(videoProgress)) { if ((currentTime / duration) >= 0.90) { // Mark as watched at 90%
videoProgress = 0; var watched = true;
}
} else { // If video is more than 30 min
if (currentTime >= (duration - 120)) { // Mark as watched if there is two minutes left
var watched = true;
}
} }
var data = { return watched;
"data": [{ }
"youtube_id": videoId,
"player": { // Runs on video pause. Sends current position.
"progress": videoProgress function onVideoPause() {
} var videoId = getVideoPlayerVideoId();
}] var currentTime = getVideoPlayerCurrentTime();
}; postVideoProgress(videoId, currentTime);
videoData = apiRequest(apiEndpoint, "POST", data);
} }
// Format numbers for frontend // Format numbers for frontend
@ -435,27 +496,34 @@ function formatNumbers(number) {
return numberFormatted; return numberFormatted;
} }
// Gets video data in JSON format when passed video ID // Gets video data when passed video ID
function getVideoData(videoId) { function getVideoData(videoId) {
var apiEndpoint = "/api/video/" + videoId + "/"; var apiEndpoint = "/api/video/" + videoId + "/";
videoData = apiRequest(apiEndpoint, "GET"); var videoData = apiRequest(apiEndpoint, "GET");
return videoData.data; return videoData;
} }
// Gets channel data in JSON format when passed channel ID // Gets channel data when passed channel ID
function getChannelData(channelId) { function getChannelData(channelId) {
var apiEndpoint = "/api/channel/" + channelId + "/"; var apiEndpoint = "/api/channel/" + channelId + "/";
channelData = apiRequest(apiEndpoint, "GET"); var channelData = apiRequest(apiEndpoint, "GET");
return channelData.data; return channelData.data;
} }
// Gets playlist data in JSON format when passed playlist ID // Gets playlist data when passed playlist ID
function getPlaylistData(playlistId) { function getPlaylistData(playlistId) {
var apiEndpoint = "/api/playlist/" + playlistId + "/"; var apiEndpoint = "/api/playlist/" + playlistId + "/";
playlistData = apiRequest(apiEndpoint, "GET"); var playlistData = apiRequest(apiEndpoint, "GET");
return playlistData.data; return playlistData.data;
} }
// Get video progress data when passed video ID
function getVideoProgress(videoId) {
var apiEndpoint = "/api/video/" + videoId + "/progress/";
var videoProgress = apiRequest(apiEndpoint, "GET");
return videoProgress;
}
// Given an array of playlist ids it returns an array of subbed playlist ids from that list // Given an array of playlist ids it returns an array of subbed playlist ids from that list
function getSubbedPlaylists(videoPlaylists) { function getSubbedPlaylists(videoPlaylists) {
var subbedPlaylists = []; var subbedPlaylists = [];
@ -467,18 +535,43 @@ function getSubbedPlaylists(videoPlaylists) {
return subbedPlaylists; return subbedPlaylists;
} }
// Makes api requests when passed an endpoint and method ("GET" or "POST") // Send video position when given video id and progress in seconds
function postVideoProgress(videoId, videoProgress) {
var apiEndpoint = "/api/video/" + videoId + "/progress/";
if (!isNaN(videoProgress)) {
var data = {
"position": videoProgress
};
if (videoProgress == 0) {
apiRequest(apiEndpoint, "DELETE");
console.log("Deleting Video Progress for Video ID: " + videoId + ", Progress: " + videoProgress);
} else if (!getVideoPlayerWatchStatus()) {
apiRequest(apiEndpoint, "POST", data);
console.log("Saving Video Progress for Video ID: " + videoId + ", Progress: " + videoProgress);
}
}
}
// Makes api requests when passed an endpoint and method ("GET", "POST", "DELETE")
function apiRequest(apiEndpoint, method, data) { function apiRequest(apiEndpoint, method, data) {
const xhttp = new XMLHttpRequest(); const xhttp = new XMLHttpRequest();
var sessionToken = getCookie("sessionid"); var sessionToken = getCookie("sessionid");
xhttp.open(method, apiEndpoint, false); xhttp.open(method, apiEndpoint, false);
xhttp.setRequestHeader("X-CSRFToken", getCookie("csrftoken")); // Used for video progress POST requests
xhttp.setRequestHeader("Authorization", "Token " + sessionToken); xhttp.setRequestHeader("Authorization", "Token " + sessionToken);
xhttp.setRequestHeader("Content-Type", "application/json"); xhttp.setRequestHeader("Content-Type", "application/json");
xhttp.send(JSON.stringify(data)); xhttp.send(JSON.stringify(data));
return JSON.parse(xhttp.responseText); return JSON.parse(xhttp.responseText);
} }
function getURL() {
return window.location.href.replace(window.location.pathname, "");
}
function removePlayer() { function removePlayer() {
var currentTime = getVideoPlayerCurrentTime();
var videoId = getVideoPlayerVideoId();
postVideoProgress(videoId, currentTime);
var playerElement = document.getElementById('player'); var playerElement = document.getElementById('player');
if (playerElement.hasChildNodes()) { if (playerElement.hasChildNodes()) {
var youtubeId = playerElement.childNodes[1].getAttribute("data-id"); var youtubeId = playerElement.childNodes[1].getAttribute("data-id");