Skip to content

Commit

Permalink
Transcript Search Functionality (#497)
Browse files Browse the repository at this point in the history
* #THM-798 begin to implement search ui in TranscriptMenu

* #THM-798 scaffold TranscriptMenu styles, basic ux flow

* #THM-798 start to integrate filtered search results

* #THM-799 clean up some inconsistent usage of refs/state in Transcript

* #THM-799 split transcript list into separate component to make things more readable

* #THM-799 simple search & highlighting

* #THM-799 hoist focusedMatchIndex to Transcript component

* #THM-799 revise mechanism behind decoration of active items so it does not conflict with filtered results

* #THM-799 make search navigator (prev/next match) scroll the transcript list

* #THM-799 restore click transcript line to scrub video

* #THM-801 basic marker implementation

* #THM-801 add some styles for search markers

* #THM-801 use classnames instead of inline styles so different types of markers can coexist

* #THM-801 rework imperative handling of handling video markers -- make them state driven

* #THM-802 wrap Transcript tests with PlayerProvider because useFilterTranscripts depends on PlayerContext

* #THM-802 fix most of the failing tests

* #THM-802 ensure that untimed transcript lines with of a timed transcript can be activated via click

* #THM-802 extract focused match management into its own hook

* #THM-802 add tests for TranscriptSearch component

* #THM-802 add tests for useFilteredTranscripts

* #THM-802 expose search options via a single prop on <Transcript />

* #THM-802 fix some markers related to regressions

* #THM-802 add extra safety: make sure markers plugin is initiated first

* #THM-805 remove requirement of being wrapped in a PlayerContextProvider

* #THM-805 format to 2-space indent

* #THM-805 make edit suggested in PR review

* #THM-805 remove outdated code
  • Loading branch information
patrick-lienau authored May 20, 2024
1 parent 25a519f commit f18afc3
Show file tree
Hide file tree
Showing 23 changed files with 1,590 additions and 597 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/node_modules
/build
/coverage
.vscode
.vscode
yarn-error.log
2 changes: 1 addition & 1 deletion demo/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const App = ({ manifestURL }) => {

/*Reference: https://accessible-react.eevis.codes/components/tabs */
const Tabs = ({ tabValues, manifestUrl }) => {
const [activeTab, setActiveTab] = React.useState(0);
const [activeTab, setActiveTab] = React.useState(1);

let tabs = [];

Expand Down
5 changes: 5 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ global.MutationObserver = class MutationObserver {
return null;
}
};

if (typeof window.HTMLElement.prototype.scrollIntoView !== 'function') {
// jsdom doesn't implement this currently so we'll need to stub it out
window.HTMLElement.prototype.scrollIntoView = () => { /* noop; */ };
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@types/jest": "^26.0.12",
"@types/react": "^18.2.79",
"babel-jest": "^26.3.0",
"babel-loader": "^8.1.0",
"babel-plugin-module-alias": "^1.6.0",
Expand Down Expand Up @@ -82,6 +83,7 @@
"dependencies": {
"@rollup/plugin-json": "^6.0.1",
"@silvermine/videojs-quality-selector": "^1.3.1",
"classnames": "^2.5.1",
"mammoth": "^1.4.19",
"manifesto.js": "^4.1.0",
"mime-db": "^1.52.0",
Expand Down
221 changes: 95 additions & 126 deletions src/components/MediaPlayer/VideoJS/VideoJSPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ function VideoJSPlayer({
isEnded,
isPlaying,
player,
searchMarkers,
currentTime,
} = playerState;

Expand All @@ -73,6 +74,7 @@ function VideoJSPlayer({
const [startQuality, setStartQuality] = useLocalStorage('startQuality', null);
const [startMuted, setStartMuted] = useLocalStorage('startMuted', false);


const videoJSRef = React.useRef(null);
const playerRef = React.useRef(null);

Expand Down Expand Up @@ -395,6 +397,60 @@ function VideoJSPlayer({
});
};

const [fragmentMarker, setFragmentMarker] = React.useState(null);

// pretty sure this can be removed...
React.useEffect(() => {
const player = playerRef.current;
if (playlist.markers?.length > 0 && isReadyRef.current) {
// Set player duration, for markers API. The value set in the player update
// function sometimes doesn't update the duration in the markers API.
player.duration(canvasDurationRef.current);
}
}, [playerRef.current, isReadyRef.current, playlist.markers]);

// update markers in player
React.useEffect(() => {
if (playerRef.current && playerRef.current.markers && isReadyRef.current) {
// markers plugin not yet initialized
if (typeof playerRef.current.markers === 'function') {
player.markers({
markerTip: {
display: false, // true,
text: marker => marker.text
},
markerStyle: {},
markers: [],
});
}

let playlistMarkers = [];
if (playlist?.markers?.length) {
const canvasMarkers = playlist.markers.filter((m) => m.canvasIndex === canvasIndex)[0].canvasMarkers;
playlistMarkers = canvasMarkers.map((m) => ({
time: parseFloat(m.time),
text: m.value,
class: 'ramp--track-marker--playlist'
}));
}

playerRef.current.markers.removeAll();
playerRef.current.markers.add([
...(fragmentMarker ? [fragmentMarker] : []),
...searchMarkers,
...playlistMarkers,

]);
}
}, [
fragmentMarker,
searchMarkers,
canvasDuration,
canvasIndex,
playerRef.current,
isReadyRef.current
]);

/**
* Setup player with player-related information parsed from the IIIF
* Manifest Canvas. This gets called on both initial page load and each
Expand All @@ -415,51 +471,6 @@ function VideoJSPlayer({
player.volume(startVolume);
player.canvasIndex = cIndexRef.current;
player.srcIndex = srcIndex;

// Initialize markers
if (player.markers) {
if (isPlaylist) {
// For playlists set styling to be pointers
player.markers({
markerTip: {
display: true,
text: function (marker) {
return marker.text;
},
},
markerStyle: {
'border-radius': 0,
height: '0.5em',
width: '0.5em',
transform: 'rotate(-45deg)',
top: '4px',
content: '',
'border-style': 'solid',
'border-width': '0.25em 0.25em 0 0',
'background-color': 'transparent'
},
markers: [],
});
} else {
// For structured navigation set styling to be highlighting ranges
player.markers({
markerTip: {
display: true,
text: function (marker) {
return marker.text;
},
},
markerStyle: {
opacity: '0.5',
'background-color': '#80A590',
'border-radius': 0,
height: '16px',
top: '-7px',
},
markers: [],
});
}
}
});

playerLoadedMetadata(player);
Expand Down Expand Up @@ -549,25 +560,6 @@ function VideoJSPlayer({
});
};

React.useEffect(() => {
const player = playerRef.current;
if (playlist.markers?.length > 0 && isReadyRef.current) {
const playlistMarkers = playlist.markers
.filter((m) => m.canvasIndex === canvasIndex)[0].canvasMarkers;
let markersList = [];
playlistMarkers.map((m) => {
markersList.push({ time: parseFloat(m.time), text: m.value });
});

// Set player duration, for markers API. The value set in the player update
// function sometimes doesn't update the duration in the markers API.
player.duration(canvasDurationRef.current);
if (player && player.markers && isReadyRef.current) {
// Reset the markers: reset() is equivalent to removeAll() and then add()
player.markers.reset(markersList);
}
}
}, [playerRef.current, isReadyRef.current, playlist.markers]);

/**
* Setting the current time of the player when using structure navigation
Expand Down Expand Up @@ -686,57 +678,50 @@ function VideoJSPlayer({
const handleTimeUpdate = () => {
const player = playerRef.current;
if (player !== null && isReadyRef.current) {
let playerTime = player.currentTime() || currentTimeRef.current;
let playerTime = player.currentTime() ?? currentTimeRef.current;
if (hasMultiItems && srcIndexRef.current > 0) {
playerTime = playerTime + targets[srcIndexRef.current].altStart;
}
const activeSegment = getActiveSegment(playerTime);
if (activeSegment && activeIdRef.current != activeSegment['id']) {
// Set the active segment in state
setActiveId(activeSegment['id']);
manifestDispatch({ item: activeSegment, type: 'switchItem' });

// Update player markers
updatePlayerMarkers(activeSegment, player);
} else if (activeSegment === null && player.markers) {
cleanUpNav();
}
};
};

/**
* Update the player markers for the non-playlist contexts when active
* structure item is updated
* @param {Object} activeSegment current active segment
* @param {Object} player Video.js player instance
*/
const updatePlayerMarkers = (activeSegment, player) => {
if (!isPlaylist) {
if (player.markers) {
// Remove all existing structure higlights
if (!isPlaylist) {
player.markers.removeAll();
}
// Use activeSegment's start and end time for marker creation
const { start, end } = getMediaFragment(activeSegment.id, canvasDurationRef.current);
playerDispatch({
endTime: end,
startTime: start,
type: 'setTimeFragment',
});
if (start != end) {
// Set the end to canvas duration if it's greater for marker rendering
let markerEnd = end > canvasDurationRef.current ? canvasDurationRef.current : end;
player.markers.add([
{
time: start,
duration: markerEnd - start,
text: activeSegment.label,
},
]);
// the active segment has changed
if (activeIdRef.current !== activeSegment?.id) {
if (activeSegment === null) {
/**
* Clear currentNavItem and other related state variables to update the tracker
* in structure navigation and highlights within the player.
*/
manifestDispatch({ item: null, type: 'switchItem' });
setActiveId(null);
} else {
// Set the active segment in state
manifestDispatch({ item: activeSegment, type: 'switchItem' });
setActiveId(activeSegment.id);

if (!isPlaylist && player.markers) {
const { start, end } = getMediaFragment(activeSegment.id, canvasDurationRef.current);
playerDispatch({
endTime: end,
startTime: start,
type: 'setTimeFragment',
});
if (start !== end) {
// don't let marker extend past the end of the canvas
let markerEnd = end > canvasDurationRef.current ? canvasDurationRef.current : end;
setFragmentMarker({
time: start,
duration: markerEnd - start,
text: start,
class: 'ramp--track-marker--fragment'
});
} else { // to prevent zero duration fragments I suppose
setFragmentMarker(null);
}
} else if (fragmentMarker !== null) {
setFragmentMarker(null);
}
}
}
}
};
};

/**
Expand Down Expand Up @@ -765,23 +750,7 @@ function VideoJSPlayer({
touchY = e.touches[0].clientY;
};

/**
* Clear currentNavItem and other related state variables to update the tracker
* in structure navigation and highlights within the player.
*/
const cleanUpNav = () => {
if (currentNavItemRef.current) {
manifestDispatch({ item: null, type: 'switchItem' });
}
setActiveId(null);
const player = playerRef.current;
if (player.markers) {
// Remove all existing structure higlights
if (!isPlaylist) {
player.markers.removeAll();
}
}
};


/**
* Get the segment, which encapsulates the current time of the playhead,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,13 @@ function TrackScrubberButton({ player, trackScrubberRef, timeToolRef, isPlaylist
In playlists, markers are timepoint information representing highlighting annotations,
therefore omit reading markers information for track scrubber in playlist contexts.
*/
if (player.markers && player.markers.getMarkers()?.length > 0 && !isPlaylist) {
const track = player.markers.getMarkers()[0];
if (track.key != currentTrack?.key) {
setCurrentTrack(track);
if (player.markers && typeof player.markers !== 'function' && !isPlaylist) {
const markers = player.markers.getMarkers();
if (markers.length) {
const track = markers.filter(m => m.class == 'ramp--track-marker--fragment');
if (track?.length > 0 && track[0].key !== currentTrack?.key) {
setCurrentTrack(track[0]);
}
}
}
/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@
height: 4px;
margin: 0;
cursor: pointer;

&:hover {
transform: scale(1, 1) !important;
}
}

/* --------- CSS for slider track for different browsers --------- */
Expand Down Expand Up @@ -69,6 +65,7 @@
/* --------- CSS for slider track thumb for different browsers --------- */
.vjs-custom-progress::-webkit-slider-thumb {
-webkit-appearance: none;
position: relative;
appearance: none;
cursor: pointer;
background: $primaryLight;
Expand All @@ -83,6 +80,7 @@
// For Firefox browsers
.vjs-custom-progress::-moz-range-thumb {
-moz-appearance: none;
position: relative;
appearance: none;
cursor: pointer;
background: $primaryLight;
Expand All @@ -97,6 +95,7 @@
// For IE/Edge browsers
.vjs-custom-progress::-ms-thumb {
appearance: none;
position: relative;
cursor: pointer;
background: $primaryLight;
border-radius: 5px;
Expand Down Expand Up @@ -160,4 +159,4 @@
border-width: 5px;
border-style: solid;
border-color: $primaryDark transparent transparent transparent;
}
}
Loading

0 comments on commit f18afc3

Please sign in to comment.