diff --git a/docs/images/Demo1.png b/docs/images/Demo1.png new file mode 100644 index 00000000..2d2e4034 Binary files /dev/null and b/docs/images/Demo1.png differ diff --git a/docs/images/Demo2.png b/docs/images/Demo2.png new file mode 100644 index 00000000..be95662b Binary files /dev/null and b/docs/images/Demo2.png differ diff --git a/docs/images/Demo3.png b/docs/images/Demo3.png new file mode 100644 index 00000000..6d83ccac Binary files /dev/null and b/docs/images/Demo3.png differ diff --git a/docs/user_docs.md b/docs/user_docs.md index cfb3c37b..3fc62ff0 100644 --- a/docs/user_docs.md +++ b/docs/user_docs.md @@ -5,6 +5,7 @@ - [Downloading media](#downloading-media) - [Adding captions/subtitles](#adding-captionssubtitles) - [Search media](#search-media) +- [Using Timestamps for sharing](#-using-timestamp) - [Share media](#share-media) - [Embed media](#embed-media) - [Customize my profile options](#customize-my-profile-options) @@ -195,6 +196,30 @@ You can now watch the captions/subtitles play back in the video player - and tog

+## Using Timestamps for sharing + +### Using Timestamp in the URL + +An additionnal Get parameter 't' can be added in video URL's to start the video at the given time. The starting time has to be given in seconds. + +

+ +

+ +Additionnally the share button has an option to generate the URL with the timestamp at current second the video is. + +

+ +

+ +### Using Timestamp in the comments + +Comments can also include timestamps. They are automatically detected upon posting the comment, and will be in the form of an hyperlink link in the comment. The timestamps in the comments have to follow the format HH:MM:SS or MM:SS + +

+ +

+ ## Search media How search can be used diff --git a/frontend/src/static/js/components/comments/Comments.jsx b/frontend/src/static/js/components/comments/Comments.jsx index c99cc452..f59fff99 100644 --- a/frontend/src/static/js/components/comments/Comments.jsx +++ b/frontend/src/static/js/components/comments/Comments.jsx @@ -368,8 +368,36 @@ export default function CommentsList(props) { const [displayComments, setDisplayComments] = useState(false); function onCommentsLoad() { - displayCommentsRelatedAlert(); - setComments([...MediaPageStore.get('media-comments')]); + const retrievedComments = [...MediaPageStore.get('media-comments')]; + + retrievedComments.forEach(comment => { + comment.text = setTimestampAnchors(comment.text); + }); + + displayCommentsRelatedAlert(); + setComments(retrievedComments); + } + + function setTimestampAnchors(text) + { + function wrapTimestampWithAnchor(match, string) + { + let split = match.split(':'), s = 0, m = 1; + let searchParameters = new URLSearchParams(window.location.search); + + while (split.length > 0) + { + s += m * parseInt(split.pop(), 10); + m *= 60; + } + + searchParameters.set('t', s) + const wrapped = "" + match + ""; + return wrapped; + } + + const timeRegex = new RegExp('((\\d)?\\d:)?(\\d)?\\d:\\d\\d', 'g'); + return text.replace(timeRegex , wrapTimestampWithAnchor); } function onCommentSubmit(commentId) { diff --git a/frontend/src/static/js/components/media-actions/MediaShareOptions.jsx b/frontend/src/static/js/components/media-actions/MediaShareOptions.jsx index cd1ca414..f4c131e8 100644 --- a/frontend/src/static/js/components/media-actions/MediaShareOptions.jsx +++ b/frontend/src/static/js/components/media-actions/MediaShareOptions.jsx @@ -182,9 +182,27 @@ function updateDimensions() { }; } +function getTimestamp() { + const videoPlayer = document.getElementsByTagName("video"); + return videoPlayer[0]?.currentTime; +} + +function ToHHMMSS (timeInt) { + let sec_num = parseInt(timeInt, 10); + let hours = Math.floor(sec_num / 3600); + let minutes = Math.floor((sec_num - (hours * 3600)) / 60); + let seconds = sec_num - (hours * 3600) - (minutes * 60); + + if (hours < 10) {hours = "0"+hours;} + if (minutes < 10) {minutes = "0"+minutes;} + if (seconds < 10) {seconds = "0"+seconds;} + return hours >= 1 ? hours + ':' + minutes + ':' + seconds : minutes + ':' + seconds; +} + export function MediaShareOptions(props) { const containerRef = useRef(null); const shareOptionsInnerRef = useRef(null); + const mediaUrl = MediaPageStore.get('media-url'); const [inlineSlider, setInlineSlider] = useState(null); const [sliderButtonsVisible, setSliderButtonsVisible] = useState({ prev: false, next: false }); @@ -192,6 +210,12 @@ export function MediaShareOptions(props) { const [dimensions, setDimensions] = useState(updateDimensions()); const [shareOptions] = useState(ShareOptions()); + const [timestamp, setTimestamp] = useState(0); + const [formattedTimestamp, setFormattedTimestamp] = useState(0); + const [startAtSelected, setStartAtSelected] = useState(false); + + const [shareMediaLink, setShareMediaLink] = useState(mediaUrl); + function onWindowResize() { setDimensions(updateDimensions()); } @@ -219,6 +243,17 @@ export function MediaShareOptions(props) { }); } + function updateStartAtCheckbox() { + setStartAtSelected(!startAtSelected); + updateShareMediaLink(); + } + + function updateShareMediaLink() + { + const newLink = startAtSelected ? mediaUrl : mediaUrl + "&t=" + Math.trunc(timestamp); + setShareMediaLink(newLink); + } + function nextSlide() { inlineSlider.nextSlide(); updateSlider(); @@ -243,6 +278,10 @@ export function MediaShareOptions(props) { useEffect(() => { PageStore.on('window_resize', onWindowResize); MediaPageStore.on('copied_media_link', onCompleteCopyMediaLink); + + const localTimestamp = getTimestamp(); + setTimestamp(localTimestamp); + setFormattedTimestamp(ToHHMMSS(localTimestamp)); return () => { PageStore.removeListener('window_resize', onWindowResize); @@ -273,10 +312,22 @@ export function MediaShareOptions(props) {
- +
+
+ +
); -} +} \ No newline at end of file diff --git a/frontend/src/static/js/components/video-player/VideoPlayer.jsx b/frontend/src/static/js/components/video-player/VideoPlayer.jsx index c83f00a4..28f17581 100644 --- a/frontend/src/static/js/components/video-player/VideoPlayer.jsx +++ b/frontend/src/static/js/components/video-player/VideoPlayer.jsx @@ -192,6 +192,13 @@ export function VideoPlayer(props) { document.addEventListener('visibilitychange', initPlayer); } + player.player.one('loadedmetadata', () => { + const urlParams = new URLSearchParams(window.location.search); + const paramT = Number(urlParams.get('t')); + const timestamp = !isNaN(paramT) ? paramT : 0; + player.player.currentTime(timestamp); + }); + return () => { unsetPlayer();