cinera.c: Add "direct" video support

Fixes:
• Rename Annotation → Timestamp in cinera_player_*.js
• Redo VideoIsPrivate() to only return once, at the end
This commit is contained in:
Matt Mascarenhas 2021-07-07 16:33:54 +01:00
parent 012d1608a0
commit c839e2ed85
4 changed files with 135 additions and 51 deletions

View File

@ -22,8 +22,8 @@ typedef struct
version CINERA_APP_VERSION = { version CINERA_APP_VERSION = {
.Major = 0, .Major = 0,
.Minor = 9, .Minor = 10,
.Patch = 2 .Patch = 0
}; };
#include <stdarg.h> // NOTE(matt): varargs #include <stdarg.h> // NOTE(matt): varargs
@ -103,7 +103,8 @@ clock_t TIMING_START;
#define MAX_BASE_DIR_LENGTH 128 #define MAX_BASE_DIR_LENGTH 128
#define MAX_BASE_URL_LENGTH 128 #define MAX_BASE_URL_LENGTH 128
#define MAX_RELATIVE_PAGE_LOCATION_LENGTH 32 #define MAX_RELATIVE_PAGE_LOCATION_LENGTH 32
#define MAX_VOD_ID_LENGTH 32 #define MAX_VOD_ID_LENGTH 256 // TODO(matt): This was 32, upped to 256 to fit full paths of "raw" videos.
// It hasn't mattered, but will do when the database contains everything.
#define MAX_ROOT_DIR_LENGTH 128 #define MAX_ROOT_DIR_LENGTH 128
#define MAX_ROOT_URL_LENGTH 128 #define MAX_ROOT_URL_LENGTH 128
@ -1493,7 +1494,7 @@ fill the slots left vacant by positioned roles in the order in which they are co
{ "title_suffix", "Currently not implemented, probably to be removed." }, { "title_suffix", "Currently not implemented, probably to be removed." },
{ "unit", "This works in conjunction with the numbering_scheme. It is a freely-configurable string - e.g. \"Day\", \"Session\", \"Episode\", \"Meeting\" - which is written on the search page, \ { "unit", "This works in conjunction with the numbering_scheme. It is a freely-configurable string - e.g. \"Day\", \"Session\", \"Episode\", \"Meeting\" - which is written on the search page, \
preceding the derived number of each entry. If the unit is not set, then the entries will not be numbered." }, preceding the derived number of each entry. If the unit is not set, then the entries will not be numbered." },
{ "vod_platform", "Possible VOD platforms: \"vimeo\", \"youtube\"." }, { "vod_platform", "Possible VOD platforms: \"direct\" (for .webm, .mp4, etc. files), \"vimeo\", \"youtube\"." },
{ "url", "The URL where viewers may support the person, e.g. their page on a crowd funding site, the \"pledge\" page on their own website." }, { "url", "The URL where viewers may support the person, e.g. their page on a crowd funding site, the \"pledge\" page on their own website." },
}; };
@ -2378,6 +2379,7 @@ GetIconTypeFromString(string *Filename, token *T)
char *VODPlatformStrings[] = char *VODPlatformStrings[] =
{ {
0, 0,
"direct",
"vimeo", "vimeo",
"youtube", "youtube",
}; };
@ -2385,6 +2387,7 @@ char *VODPlatformStrings[] =
typedef enum typedef enum
{ {
VP_DEFAULT_UNSET, VP_DEFAULT_UNSET,
VP_DIRECT,
VP_VIMEO, VP_VIMEO,
VP_YOUTUBE, VP_YOUTUBE,
VP_COUNT, VP_COUNT,
@ -9010,12 +9013,11 @@ ExamineDB(void)
bool bool
VideoIsPrivate(vod_platform VODPlatform, char *VideoID) VideoIsPrivate(vod_platform VODPlatform, char *VideoID)
{ {
// TODO(matt): Redo this to only return once, at the end bool Result = FALSE;
// NOTE(matt): Currently only supports YouTube // NOTE(matt): Currently only supports YouTube
// NOTE(matt): Stack-string
if(VODPlatform == VP_YOUTUBE) if(VODPlatform == VP_YOUTUBE)
{ {
// NOTE(matt): Stack-string
char Message[128]; char Message[128];
CopyString(Message, sizeof(Message), "%sChecking%s privacy status of: https://youtube.com/watch?v=%s", ColourStrings[CS_ONGOING], ColourStrings[CS_END], VideoID); CopyString(Message, sizeof(Message), "%sChecking%s privacy status of: https://youtube.com/watch?v=%s", ColourStrings[CS_ONGOING], ColourStrings[CS_END], VideoID);
fprintf(stderr, "%s", Message); fprintf(stderr, "%s", Message);
@ -9047,12 +9049,11 @@ VideoIsPrivate(vod_platform VODPlatform, char *VideoID)
SeekBufferForString(&VideoAPIResponse, "\"totalResults\": ", C_SEEK_FORWARDS, C_SEEK_AFTER); SeekBufferForString(&VideoAPIResponse, "\"totalResults\": ", C_SEEK_FORWARDS, C_SEEK_AFTER);
if(*VideoAPIResponse.Ptr == '0') if(*VideoAPIResponse.Ptr == '0')
{ {
DeclaimBuffer(&VideoAPIResponse);
// printf("Private video: https://youtube.com/watch?v=%s\n", VideoID); // printf("Private video: https://youtube.com/watch?v=%s\n", VideoID);
ClearTerminalRow(MessageLength); Result = TRUE;
return TRUE;
} }
else
{
VideoAPIResponse.Ptr = VideoAPIResponse.Location; VideoAPIResponse.Ptr = VideoAPIResponse.Location;
SeekBufferForString(&VideoAPIResponse, "{", C_SEEK_FORWARDS, C_SEEK_AFTER); SeekBufferForString(&VideoAPIResponse, "{", C_SEEK_FORWARDS, C_SEEK_AFTER);
SeekBufferForString(&VideoAPIResponse, "\"privacyStatus\": \"", C_SEEK_FORWARDS, C_SEEK_AFTER); SeekBufferForString(&VideoAPIResponse, "\"privacyStatus\": \"", C_SEEK_FORWARDS, C_SEEK_AFTER);
@ -9061,20 +9062,19 @@ VideoIsPrivate(vod_platform VODPlatform, char *VideoID)
CopyStringNoFormatT(Status, sizeof(Status), VideoAPIResponse.Ptr, '\"'); CopyStringNoFormatT(Status, sizeof(Status), VideoAPIResponse.Ptr, '\"');
if(!StringsDiffer0(Status, "public")) if(!StringsDiffer0(Status, "public"))
{ {
DeclaimBuffer(&VideoAPIResponse); Result = FALSE;
ClearTerminalRow(MessageLength); }
return FALSE; else
{
Result = TRUE;
}
} }
} }
DeclaimBuffer(&VideoAPIResponse); DeclaimBuffer(&VideoAPIResponse);
// printf("Unlisted video: https://youtube.com/watch?v=%s\n", VideoID); // printf("Unlisted video: https://youtube.com/watch?v=%s\n", VideoID);
ClearTerminalRow(MessageLength); ClearTerminalRow(MessageLength);
return TRUE;
}
else
{
return FALSE;
} }
return Result;
} }
bool bool

View File

@ -823,6 +823,14 @@ ul.cineraNavPlain li.current > a {
overflow: hidden; overflow: hidden;
} }
.cineraPlayerContainer .video_container .direct_video {
display: flex;
}
.cineraPlayerContainer .video_container .direct_video video {
outline: none;
}
.cineraPlayerContainer .markers_container { .cineraPlayerContainer .markers_container {
flex-shrink: 1; flex-shrink: 1;
overflow-y: scroll; overflow-y: scroll;

View File

@ -206,7 +206,7 @@ if(titleBar)
} }
linkMenu = titleBar.querySelector(".link_container"); linkMenu = titleBar.querySelector(".link_container");
linkAnnotation = true; linkTimestamp = true;
if(linkMenu) if(linkMenu)
{ {
menuState.push(linkMenu); menuState.push(linkMenu);
@ -382,12 +382,12 @@ for(var i = 0; i < topicDots.length; ++i)
setDotLightness(topicDots[i]); setDotLightness(topicDots[i]);
} }
var lastAnnotationStorageItem = "cineraTimecode_" + window.location.pathname; var lastTimestampStorageItem = "cineraTimecode_" + window.location.pathname;
var lastAnnotation; var lastTimestamp;
if(location.hash) { if(location.hash) {
player.setTimeThenPlay(location.hash.startsWith('#') ? location.hash.substr(1) : location.hash); player.setTimeThenPlay(location.hash.startsWith('#') ? location.hash.substr(1) : location.hash);
} }
else if(lastAnnotation = localStorage.getItem(lastAnnotationStorageItem)) else if(lastTimestamp = localStorage.getItem(lastTimestampStorageItem))
{ {
player.setTimeThenPlay(lastAnnotation); player.setTimeThenPlay(lastTimestamp);
} }

View File

@ -2,8 +2,9 @@
// Will be called when the player enters a marker that has a `data-ref` attribute. The value of `data-ref` will be passed to the function. // Will be called when the player enters a marker that has a `data-ref` attribute. The value of `data-ref` will be passed to the function.
// When leaving a marker that a `data-ref` attribute, and entering a marker without one (or not entering a new marker at all), the function will be called with `null`. // When leaving a marker that a `data-ref` attribute, and entering a marker without one (or not entering a new marker at all), the function will be called with `null`.
var vod_platform = { var vod_platform = {
VIMEO: 0, DIRECT: 0,
YOUTUBE: 1, VIMEO: 1,
YOUTUBE: 2,
}; };
function function
@ -12,6 +13,7 @@ GetVODPlatformFromString(VODPlatformString)
var Result = null; var Result = null;
switch(VODPlatformString) switch(VODPlatformString)
{ {
case "direct": Result = vod_platform.DIRECT; break;
case "vimeo": Result = vod_platform.VIMEO; break; case "vimeo": Result = vod_platform.VIMEO; break;
case "youtube": Result = vod_platform.YOUTUBE; break; case "youtube": Result = vod_platform.YOUTUBE; break;
default: break; default: break;
@ -58,6 +60,7 @@ function Player(htmlContainer, refsCallback) {
this.platformPlayer = null; this.platformPlayer = null;
this.platformPlayerReady = false; this.platformPlayerReady = false;
this.duration = null;
this.playing = false; this.playing = false;
this.shouldPlay = false; this.shouldPlay = false;
this.buffering = false; this.buffering = false;
@ -88,6 +91,10 @@ Player.prototype.play = function() {
if (!this.playing) { if (!this.playing) {
switch(this.vod_platform) switch(this.vod_platform)
{ {
case vod_platform.DIRECT:
{
this.platformPlayer.play();
} break;
case vod_platform.VIMEO: case vod_platform.VIMEO:
{ {
this.platformPlayer.play(); this.platformPlayer.play();
@ -111,6 +118,10 @@ Player.prototype.pause = function() {
if (this.playing) { if (this.playing) {
switch(this.vod_platform) switch(this.vod_platform)
{ {
case vod_platform.DIRECT:
{
this.platformPlayer.pause();
} break;
case vod_platform.VIMEO: case vod_platform.VIMEO:
{ {
this.platformPlayer.pause(); this.platformPlayer.pause();
@ -134,6 +145,15 @@ Player.prototype.setTimeThenPlay = function(time) {
this.currentTime = time; this.currentTime = time;
switch(this.vod_platform) switch(this.vod_platform)
{ {
case vod_platform.DIRECT:
{
if (this.platformPlayerReady) {
this.currentTime = Math.max(0, Math.min(this.currentTime, this.duration));
this.platformPlayer.currentTime = this.currentTime;
this.updateProgress();
this.play();
}
} break;
case vod_platform.VIMEO: case vod_platform.VIMEO:
{ {
if (this.platformPlayerReady) { if (this.platformPlayerReady) {
@ -537,7 +557,7 @@ ConnectMobileControls(player)
}); });
} }
// Call this after changing the size of the video container in order to update the youtube player. // Call this after changing the size of the video container in order to update the platform player.
Player.prototype.updateSize = function() { Player.prototype.updateSize = function() {
var width = 0; var width = 0;
var height = 0; var height = 0;
@ -567,17 +587,23 @@ Player.prototype.updateSize = function() {
height = this.videoContainer.offsetHeight; height = this.videoContainer.offsetHeight;
} }
if(this.platformPlayerReady)
{
switch(this.vod_platform) switch(this.vod_platform)
{ {
case vod_platform.VIMEO: /* NOTE(matt): It responds automatically */ break; case vod_platform.DIRECT:
{
this.platformPlayer.setAttribute("width", Math.floor(width));
this.platformPlayer.setAttribute("height", Math.floor(height));
} break;
case vod_platform.VIMEO: break; // NOTE(matt): It responds automatically
case vod_platform.YOUTUBE: case vod_platform.YOUTUBE:
{ {
if (this.platformPlayerReady) {
this.platformPlayer.setSize(Math.floor(width), Math.floor(height)); this.platformPlayer.setSize(Math.floor(width), Math.floor(height));
}
} break; } break;
default: break; default: break;
} }
}
} }
// Stops the per-frame work that the player does. Call when you want to hide or get rid of the player. // Stops the per-frame work that the player does. Call when you want to hide or get rid of the player.
@ -601,6 +627,7 @@ Player.prototype.resume = function() {
Player.initializePlatform = function(platform_id, callback) { Player.initializePlatform = function(platform_id, callback) {
switch(platform_id) switch(platform_id)
{ {
case vod_platform.DIRECT:
case vod_platform.VIMEO: case vod_platform.VIMEO:
{ {
callback(); callback();
@ -688,11 +715,11 @@ Player.prototype.updateProgress = function() {
if (this.currentMarker) { if (this.currentMarker) {
if(this.currentMarkerIdx == this.markers.length - 1) if(this.currentMarkerIdx == this.markers.length - 1)
{ {
localStorage.removeItem(lastAnnotationStorageItem); localStorage.removeItem(lastTimestampStorageItem);
} }
else else
{ {
localStorage.setItem(lastAnnotationStorageItem, this.currentMarker.timestamp); localStorage.setItem(lastTimestampStorageItem, this.currentMarker.timestamp);
} }
this.currentMarker.el.classList.add("current"); this.currentMarker.el.classList.add("current");
this.scrollTo = this.currentMarker.el.offsetTop + this.currentMarker.el.offsetHeight/2.0; this.scrollTo = this.currentMarker.el.offsetTop + this.currentMarker.el.offsetHeight/2.0;
@ -708,6 +735,11 @@ Player.prototype.doFrame = function() {
if (this.playing) { if (this.playing) {
switch(this.vod_platform) switch(this.vod_platform)
{ {
case vod_platform.DIRECT:
{
this.currentTime = this.platformPlayer.currentTime;
this.updateProgress();
} break;
case vod_platform.VIMEO: case vod_platform.VIMEO:
{ {
var Parent = this; var Parent = this;
@ -744,6 +776,10 @@ Player.prototype.doFrame = function() {
Player.prototype.setStyleAndQuality = function() { Player.prototype.setStyleAndQuality = function() {
switch(this.vod_platform) switch(this.vod_platform)
{ {
case vod_platform.DIRECT:
{
// NOTE(matt): onPlatformReady() has set the width and height
} break;
case vod_platform.VIMEO: case vod_platform.VIMEO:
{ {
this.platformPlayer.setQuality("1080p"); this.platformPlayer.setQuality("1080p");
@ -777,6 +813,10 @@ Player.prototype.setDurationThenAutoplay = function(Duration) {
Player.prototype.acquireDurationThenAutoplay = function() { Player.prototype.acquireDurationThenAutoplay = function() {
switch(this.vod_platform) switch(this.vod_platform)
{ {
case vod_platform.DIRECT:
{
this.setDurationThenAutoplay(this.platformPlayer.duration);
} break;
case vod_platform.VIMEO: case vod_platform.VIMEO:
{ {
var Parent = this; var Parent = this;
@ -804,6 +844,10 @@ Player.prototype.onPlaybackRateChange = function(Rate)
this.speed = Rate; this.speed = Rate;
} }
Player.prototype.onDirectPlayerPlaybackRateChange = function(ev) {
this.onPlaybackRateChange(this.platformPlayer.playbackRate);
};
Player.prototype.onVimeoPlayerPlaybackRateChange = function(ev) { Player.prototype.onVimeoPlayerPlaybackRateChange = function(ev) {
this.onPlaybackRateChange(ev.playbackRate); this.onPlaybackRateChange(ev.playbackRate);
}; };
@ -815,7 +859,6 @@ Player.prototype.onYouTubePlayerPlaybackRateChange = function(ev) {
Player.prototype.onStateBufferStart = function() Player.prototype.onStateBufferStart = function()
{ {
this.buffering = true; this.buffering = true;
this.playing = false;
this.updateProgress(); this.updateProgress();
} }
@ -829,7 +872,7 @@ Player.prototype.onStateEnded = function()
{ {
this.buffering = false; this.buffering = false;
this.playing = false; this.playing = false;
localStorage.removeItem(lastAnnotationStorageItem); localStorage.removeItem(lastTimestampStorageItem);
this.currentTime = null; this.currentTime = null;
this.updateProgress(); this.updateProgress();
} }
@ -854,6 +897,16 @@ Player.prototype.onStatePlaying = function(CurrentTime)
} }
} }
Player.prototype.onDirectPlayerStateChange_Paused = function(ev)
{
this.onStatePaused(this.platformPlayer.currentTime);
}
Player.prototype.onDirectPlayerStateChange_Playing = function(ev)
{
this.onStatePlaying(this.platformPlayer.currentTime);
}
Player.prototype.onVimeoPlayerStateChange_Paused = function(ev) Player.prototype.onVimeoPlayerStateChange_Paused = function(ev)
{ {
this.onStatePaused(ev.seconds); this.onStatePaused(ev.seconds);
@ -878,10 +931,29 @@ Player.prototype.onYouTubePlayerStateChange = function(ev) {
Player.prototype.onPlatformReady = function() { Player.prototype.onPlatformReady = function() {
var platformPlayerDiv = document.createElement("DIV"); var platformPlayerDiv = document.createElement("DIV");
platformPlayerDiv.id = "platform_player_" + Player.platformPlayerCount++; platformPlayerDiv.id = "platform_player_" + Player.platformPlayerCount++;
this.videoContainer.appendChild(platformPlayerDiv); var platformPlayerDivPlaced = this.videoContainer.appendChild(platformPlayerDiv);
switch(this.vod_platform) switch(this.vod_platform)
{ {
case vod_platform.DIRECT:
{
platformPlayerDivPlaced.classList.add("direct_video");
var videoNode = document.createElement("VIDEO");
videoNode.controls = true;
videoNode.setAttribute("width", this.videoContainer.offsetWidth);
videoNode.setAttribute("height", this.videoContainer.offsetWidth / 16 * 9);
var videoSourceNode = document.createElement("SOURCE");
videoSourceNode.setAttribute("src", this.videoContainer.getAttribute("data-videoId"));
videoNode.appendChild(videoSourceNode);
this.platformPlayer = platformPlayerDiv.appendChild(videoNode);
this.platformPlayer.addEventListener("loadedmetadata", this.onPlatformPlayerReady.bind(this));
this.platformPlayer.addEventListener("ratechange", this.onDirectPlayerPlaybackRateChange.bind(this));
this.platformPlayer.addEventListener("play", this.onDirectPlayerStateChange_Playing.bind(this));
this.platformPlayer.addEventListener("pause", this.onDirectPlayerStateChange_Paused.bind(this));
this.platformPlayer.addEventListener("waiting", this.onStateBufferStart.bind(this));
this.platformPlayer.addEventListener("playing", this.onStateBufferEnd.bind(this));
this.platformPlayer.addEventListener("ended", this.onStateEnded.bind(this));
} break;
case vod_platform.VIMEO: case vod_platform.VIMEO:
{ {
this.videoContainer.style.position = "relative"; this.videoContainer.style.position = "relative";
@ -938,7 +1010,7 @@ function updateLink()
{ {
if(link && player) if(link && player)
{ {
if(linkAnnotation == true) if(linkTimestamp == true)
{ {
if(player.currentMarker) if(player.currentMarker)
{ {
@ -953,6 +1025,10 @@ function updateLink()
{ {
switch(player.vod_platform) switch(player.vod_platform)
{ {
case vod_platform.DIRECT:
{
link.value = baseURL + "#" + Math.round(player.platformPlayer.currentTime);
} break;
case vod_platform.VIMEO: case vod_platform.VIMEO:
{ {
player.platformPlayer.getCurrentTime() player.platformPlayer.getCurrentTime()
@ -972,8 +1048,8 @@ function updateLink()
function toggleLinkMode(linkMode, link) function toggleLinkMode(linkMode, link)
{ {
linkAnnotation = !linkAnnotation; linkTimestamp = !linkTimestamp;
if(linkAnnotation == true) if(linkTimestamp == true)
{ {
linkMode.textContent = "Link to: current timestamp"; linkMode.textContent = "Link to: current timestamp";
} }
@ -1721,7 +1797,7 @@ function handleKey(key) {
case 'Y': { case 'Y': {
if(cineraLink) if(cineraLink)
{ {
if(linkAnnotation == false && player.playing) if(linkTimestamp == false && player.playing)
{ {
player.pause(); player.pause();
} }