Skip to content

Commit

Permalink
Merge branch 'Simon-Initiative:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
dtiwarATS committed May 27, 2024
2 parents c8c76ec + 41173ac commit bffba11
Show file tree
Hide file tree
Showing 42 changed files with 1,449 additions and 124 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const VideoEditor = (props: Props) => {
<span {...props.attributes} contentEditable={false} style={{ position: 'relative' }}>
{props.children}

<VideoPlayer video={model}>
<VideoPlayer video={model} pageAttemptGuid="">
<VideoSettings model={props.model} onEdit={onEdit} commandContext={props.commandContext} />
</VideoPlayer>
</span>
Expand Down
140 changes: 137 additions & 3 deletions assets/src/components/video_player/VideoPlayer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ReactNode, useCallback, useRef } from 'react';
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import {
BigPlayButton,
ControlBar,
Expand All @@ -11,6 +11,7 @@ import {
TimeDivider,
} from 'video-react';
import { PointMarkerContext, maybePointMarkerAttr } from 'data/content/utils';
import * as XAPI from 'data/persistence/xapi';
import * as ContentModel from '../../data/content/model/elements/types';
import { useCommandTarget } from '../editing/elements/command_button/useCommandTarget';
import { ClosedCaptionButton } from './ClosedCaptionButton';
Expand Down Expand Up @@ -55,25 +56,40 @@ interface VideoInterface {

export const VideoPlayer: React.FC<{
video: ContentModel.Video;
pageAttemptGuid: string;
pointMarkerContext?: PointMarkerContext;
children?: ReactNode;
}> = React.memo(({ video, pointMarkerContext, children }) => {
}> = React.memo(({ video, pointMarkerContext, children, pageAttemptGuid }) => {
const playerRef = useRef(null);
const pauseAtPosition = useRef(-1);
const [seekFrom, setSeekFrom] = useState(0);
const [segments, setSegments] = useState([] as XAPI.PlayedSegment[]);
const segmentsRef = useRef(segments);
const seekFromRef = useRef(seekFrom);

const sizeAttributes = isValidSize(video)
? { width: video.width, height: video.height, fluid: false }
: { fluid: true };

const hasCaptions = video.captions && video.captions.length > 0;

// Update segmentsRef whenever segments change
useEffect(() => {
segmentsRef.current = segments;
}, [segments]);

useEffect(() => {
seekFromRef.current = seekFrom;
}, [seekFrom]);

const onPlayer = useCallback((player) => {
playerRef.current = player;

if (!player) {
return;
}
// This handles stopping at the correct point if a cue-point command previously came in with an end-timestamp set.
player.subscribeToStateChange((state: PlayerState) => {
player.subscribeToStateChange((state: PlayerState, prev: PlayerState) => {
if (
pauseAtPosition.current > 0 &&
state.hasStarted &&
Expand All @@ -82,6 +98,124 @@ export const VideoPlayer: React.FC<{
pauseAtPosition.current = -1;
player.pause();
}

// In an authoring context, just stop here, we don't want to emit xAPI events
if (pageAttemptGuid === '') {
return;
}

if (state.seekingTime !== 0 && prev.seekingTime === 0) {
const lastSegment = segmentsRef.current[segmentsRef.current.length - 1];

if (lastSegment) {
lastSegment.end = state.seekingTime;
segmentsRef.current[segmentsRef.current.length - 1] = lastSegment;
setSegments(segmentsRef.current);
}

setSeekFrom(prev.currentTime);
} else if (!state.seeking && prev.seeking) {
const segment = { start: state.currentTime, end: null };
setSegments([...segmentsRef.current, segment]);

XAPI.emit_delivery(
{
type: 'page_video_key',
page_attempt_guid: pageAttemptGuid,
},
{
type: 'video_seeked',
category: 'video',
event_type: 'seeked',
video_url: state.currentSrc,
video_title: state.currentSrc,
video_seek_to: state.currentTime,
video_seek_from: seekFromRef.current,
content_element_id: video.id,
} as XAPI.VideoSeekedEvent,
);
} else if (state.ended && !prev.ended) {
const lastSegment = segmentsRef.current[segmentsRef.current.length - 1];
if (lastSegment) {
lastSegment.end = state.currentTime;
}
const segments = segmentsRef.current;
segments[segments.length - 1] = lastSegment;

const progress = XAPI.calculateProgress(segments, state.duration);

// Emit completed event
XAPI.emit_delivery(
{
type: 'page_video_key',
page_attempt_guid: pageAttemptGuid,
},
{
type: 'video_completed',
category: 'video',
event_type: 'completed',
video_url: state.currentSrc,
video_title: state.currentSrc,
video_length: state.duration,
video_played_segments: XAPI.formatSegments(segments),
video_progress: progress,
video_time: state.currentTime,
content_element_id: video.id,
} as XAPI.VideoCompletedEvent,
);
} else if (state.paused && !prev.paused) {
const lastSegment = segmentsRef.current[segmentsRef.current.length - 1];
let segmentsStr = '';

if (lastSegment) {
lastSegment.end = state.currentTime;
const segments = segmentsRef.current;
segments[segments.length - 1] = lastSegment;
setSegments(segments);
segmentsStr = XAPI.formatSegments(segments);
}

// Emit paused event
XAPI.emit_delivery(
{
type: 'page_video_key',
page_attempt_guid: pageAttemptGuid,
},
{
type: 'video_paused',
category: 'video',
event_type: 'paused',
video_url: state.currentSrc,
video_title: state.currentSrc,
video_length: state.duration,
video_played_segments: segmentsStr,
video_progress: XAPI.calculateProgress(segments, state.duration),
video_time: state.currentTime,
content_element_id: video.id,
} as XAPI.VideoPausedEvent,
);
} else if (!state.paused && prev.paused) {
const segment = { start: state.currentTime, end: null };
setSegments([...segmentsRef.current, segment]);

// Emit played event
XAPI.emit_delivery(
{
type: 'page_video_key',
page_attempt_guid: pageAttemptGuid,
},
{
type: 'video_played',
category: 'video',
event_type: 'played',
video_url: state.currentSrc,
video_title: state.currentSrc,
video_length: state.duration,
video_play_time: state.currentTime,
content_element_id: video.id,
} as XAPI.VideoPlayedEvent,
);
}
});
}, []);

Expand Down
93 changes: 93 additions & 0 deletions assets/src/components/youtube_player/YoutubePlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as ContentModel from 'data/content/model/elements/types';
import { PointMarkerContext, maybePointMarkerAttr } from 'data/content/utils';
import { WriterContext, defaultWriterContext } from 'data/content/writers/context';
import { HtmlContentModelRenderer } from 'data/content/writers/renderer';
import * as XAPI from 'data/persistence/xapi';

interface Player {
seekTo: (time: number) => void;
Expand All @@ -26,8 +27,12 @@ export const YoutubePlayer: React.FC<{
}> = ({ video, children, authorMode, context, pointMarkerContext }) => {
const stopInterval = useRef<number | undefined>();
const [videoTarget, setVideoTarget] = useState<Player | null>(null);
const segments = useRef<XAPI.PlayedSegment[]>([]);
const url = useRef<string>('');
const duration = useRef<number>(0);
const pauseAtPosition = useRef(video.endTime || -1);
const videoId = video.src || CUTE_OTTERS;
const youTubeRef = useRef<YouTube>(null);
context = context || defaultWriterContext();

const opts: Options = authorMode
Expand All @@ -53,6 +58,17 @@ export const YoutubePlayer: React.FC<{

const onReady = useCallback((event) => {
setVideoTarget(event.target);

(youTubeRef.current as any)
.getInternalPlayer()
.getDuration()
.then((d: number) => {
duration.current = d;
});
(youTubeRef.current as any)
.getInternalPlayer()
.getVideoUrl()
.then((u: string) => (url.current = u));
}, []);

const stopAtTime = useCallback(() => {
Expand Down Expand Up @@ -84,6 +100,81 @@ export const YoutubePlayer: React.FC<{
}
}, [authorMode, video.endTime, video.startTime, videoTarget]);

const onStateChange = (e: any) => {
if (!videoTarget) return;

switch (e.data) {
case 0:
XAPI.emit_delivery(
{
type: 'page_video_key',
page_attempt_guid:
context?.resourceAttemptGuid !== undefined ? context?.resourceAttemptGuid : '',
},
{
type: 'video_completed',
category: 'video',
event_type: 'completed',
video_url: url.current,
video_title: url.current,
video_length: duration.current,
video_played_segments: XAPI.formatSegments(segments.current),
video_progress: XAPI.calculateProgress(segments.current, duration.current),
video_time: videoTarget.getCurrentTime(),
content_element_id: video.id,
} as XAPI.VideoCompletedEvent,
);
break;
case 1:
const segment = { start: videoTarget.getCurrentTime(), end: null };
segments.current.push(segment);

XAPI.emit_delivery(
{
type: 'page_video_key',
page_attempt_guid:
context?.resourceAttemptGuid !== undefined ? context?.resourceAttemptGuid : '',
},
{
type: 'video_played',
category: 'video',
event_type: 'played',
video_url: url.current,
video_title: url.current,
video_length: duration.current,
video_play_time: videoTarget.getCurrentTime(),
content_element_id: video.id,
} as XAPI.VideoPlayedEvent,
);
break;
case 2:
const lastSegment = segments.current[segments.current.length - 1];
lastSegment.end = videoTarget.getCurrentTime();
segments.current[segments.current.length - 1] = lastSegment;

XAPI.emit_delivery(
{
type: 'page_video_key',
page_attempt_guid:
context?.resourceAttemptGuid !== undefined ? context?.resourceAttemptGuid : '',
},
{
type: 'video_paused',
category: 'video',
event_type: 'paused',
video_url: url.current,
video_title: url.current,
video_length: duration.current,
video_played_segments: XAPI.formatSegments(segments.current),
video_progress: XAPI.calculateProgress(segments.current, duration.current),
video_time: videoTarget.getCurrentTime(),
content_element_id: video.id,
} as XAPI.VideoPausedEvent,
);
break;
}
};

const onCommandReceived = useCallback(
(message: string) => {
if (!videoTarget) return;
Expand Down Expand Up @@ -124,10 +215,12 @@ export const YoutubePlayer: React.FC<{
{...maybePointMarkerAttr(video, pointMarkerContext)}
>
<YouTube
ref={youTubeRef}
className="embed-responsive-item"
videoId={videoId}
opts={opts}
onReady={onReady}
onStateChange={onStateChange}
/>
</div>
{!authorMode && video.caption && (
Expand Down
8 changes: 7 additions & 1 deletion assets/src/data/content/writers/html.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,13 @@ export class HtmlParser implements WriterImpl {
}

video(context: WriterContext, next: Next, v: Video) {
return <VideoPlayer video={v} pointMarkerContext={pointMarkerContextFrom(context, v)} />;
return (
<VideoPlayer
video={v}
pageAttemptGuid={context.resourceAttemptGuid as any}
pointMarkerContext={pointMarkerContextFrom(context, v)}
/>
);
}

ecl(context: WriterContext, next: Next, attrs: ECLRepl) {
Expand Down
Loading

0 comments on commit bffba11

Please sign in to comment.