Skip to content

Commit

Permalink
feature: Content progression (#2500)
Browse files Browse the repository at this point in the history
* playhead tracking

* pulling play history working.

* adjust on end.
  • Loading branch information
camrun91 committed May 12, 2022
1 parent 9ddf939 commit fbbaaea
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 23 deletions.
116 changes: 116 additions & 0 deletions packages/apollos-ui-connected/src/ApollosPlayerConnected/index.js
@@ -0,0 +1,116 @@
import React, { useCallback } from 'react';
import { useQuery, useMutation } from '@apollo/client';
import PropTypes from 'prop-types';
import { ApollosPlayerContainer } from '@apollosproject/ui-media-player';
import { named } from '@apollosproject/ui-kit/src/theme';
import { useTrack } from '@apollosproject/ui-analytics';
import { GET_MEDIA } from '../NodeSingleConnected';
import INTERACT_WITH_NODE from './interactWithNode';

const Noop = () => null;

const ApollosPlayerConnected = ({
nodeId,
Component,
audioOnly,
mediaSource,
...props
}) => {
const [interactWithVideo] = useMutation(INTERACT_WITH_NODE);

const track = useTrack();

const { data } = useQuery(GET_MEDIA, {
variables: { nodeId },
fetchPolicy: 'cache-and-network',
});

const handleOnPlay = useCallback(
({ id, elapsedTime, title }) => {
interactWithVideo({
variables: {
nodeId: id,
action: 'VIEW',
data: { field: 'progress', value: elapsedTime },
},
});
track({
eventName: 'Video Play',
properties: {
videoId: id,
videoTitle: title,
},
});
},
[interactWithVideo, track]
);
const handleOnPause = useCallback(
({ id, elapsedTime, title }) => {
interactWithVideo({
variables: {
nodeId: id,
action: 'VIEW',
data: { field: 'progress', value: elapsedTime },
},
});
track({
eventName: 'Video Pause',
properties: {
videoId: id,
videoTitle: title,
},
});
},
[interactWithVideo, track]
);
const handleOnEnd = useCallback(
({ id, elapsedTime, title }) => {
interactWithVideo({
variables: {
nodeId: id,
action: 'VIEW',
data: { field: 'progress', value: elapsedTime },
},
});
track({
eventName: 'Video End',
properties: {
videoId: id,
videoTitle: title,
},
});
},
[interactWithVideo, track]
);

return (
<ApollosPlayerContainer
source={mediaSource}
coverImage={data.node?.coverImage?.sources}
presentationProps={{
title: data?.node?.title,
}}
audioOnly={audioOnly}
videos={data?.node?.videos}
onPlay={handleOnPlay}
onPause={handleOnPause}
onEnd={handleOnEnd}
playheadStart={data?.node?.videos[0]?.userProgress?.playhead}
{...props}
>
<Component nodeId={nodeId} ImageWrapperComponent={Noop} {...props} />
</ApollosPlayerContainer>
);
};

ApollosPlayerConnected.propTypes = {
nodeId: PropTypes.string,
Component: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
audioOnly: PropTypes.bool,
// eslint-disable-next-line react/forbid-prop-types
mediaSource: PropTypes.object,
};

export default named('ui-connected.ApollosPlayerConnected')(
ApollosPlayerConnected
);
@@ -0,0 +1,13 @@
import { gql } from '@apollo/client';

export default gql`
mutation interactWithNode(
$nodeId: ID!
$action: InteractionAction!
$data: [InteractionDataField]
) {
interactWithNode(nodeId: $nodeId, action: $action, data: $data) {
success
}
}
`;
21 changes: 7 additions & 14 deletions packages/apollos-ui-connected/src/NodeSingleConnected/index.js
Expand Up @@ -10,22 +10,19 @@ import {
StretchyViewExperimental as StretchyView,
named,
} from '@apollosproject/ui-kit';
import { ApollosPlayerContainer } from '@apollosproject/ui-media-player';

import ContentNodeConnected from '../ContentNodeConnected';
import ContentParentFeedConnected from '../ContentParentFeedConnected';
import ContentChildFeedConnected from '../ContentChildFeedConnected';
import UpNextButtonConnected from '../UpNextButtonConnected';
import NodeFeaturesConnected from '../NodeFeaturesConnected';
import ScriptureNodeConnected from '../ScriptureNodeConnected';
import ApollosPlayerConnected from '../ApollosPlayerConnected';

import GET_MEDIA from './getMedia';
import GET_TITLE from './getTitle';

const Noop = () => null;

const FlexedScrollView = styled({ flex: 1 })(Reanimated.ScrollView);

const NodeSingleInner = ({ nodeId, ImageWrapperComponent, ...props }) => (
<View {...props}>
<ContentNodeConnected
Expand Down Expand Up @@ -128,18 +125,14 @@ const NodeSingleConnectedWithMedia = ({

return (
<BackgroundView>
<ApollosPlayerContainer
source={mediaSource}
coverImage={data.node?.coverImage?.sources}
presentationProps={{
title: data.node.title,
}}
<ApollosPlayerConnected
nodeId={nodeId}
mediaSource={mediaSource}
Component={Component}
audioOnly={!hasLivestream && !hasVideo && hasAudio}
videos={data?.node?.videos}
>
<Component nodeId={nodeId} ImageWrapperComponent={Noop} {...props} />
</ApollosPlayerContainer>
{children}
{children}
</ApollosPlayerConnected>
</BackgroundView>
);
};
Expand Down
6 changes: 6 additions & 0 deletions packages/apollos-ui-fragments/src/interfaces.js
Expand Up @@ -17,6 +17,12 @@ const VIDEO_NODE_FRAGMENT = gql`
sources {
uri
}
id
duration
userProgress {
playhead
complete
}
embedHtml
}
}
Expand Down
12 changes: 9 additions & 3 deletions packages/apollos-ui-media-player/src/Container.tsx
Expand Up @@ -6,6 +6,7 @@ import {
IPlayhead,
IPlayerControls,
IInternalPlayer,
IVideo,
} from './types';
import FullscreenSlidingPlayer, {
FullScreenSlidingPlayerProps,
Expand All @@ -27,6 +28,8 @@ interface ContainerProps extends IPlayerMedia, FullScreenSlidingPlayerProps {
onPlay?: Function;
onPause?: Function;
onEnd?: Function;
playheadStart?: number;
videos?: Array<IVideo>;

/** The Player Component. Defaults to FullscreenSlidingPlayer */
PlayerComponent?: React.FunctionComponent;
Expand Down Expand Up @@ -54,10 +57,11 @@ const Container: React.FunctionComponent<ContainerProps> = ({
autoplay = false,
useNativeFullscreeniOS,
scrollViewRef,
videos,
videos = [],
onPlay = () => {},
onPause = () => {},
onEnd = () => {},
playheadStart = 0,
}) => {
/*
We're going to set up 4 context state objects in this component:
Expand Down Expand Up @@ -138,7 +142,7 @@ const Container: React.FunctionComponent<ContainerProps> = ({
totalDuration: 1,
seekableDuration: 1,
playableDuration: 1,
elapsedTime: 0,
elapsedTime: playheadStart,
});

// Vincent not using useMemo here is for you 😘
Expand Down Expand Up @@ -170,7 +174,9 @@ const Container: React.FunctionComponent<ContainerProps> = ({
// we need a session id for some analytics events. The session ID should be
// created on mount, and different for each video player
const { current: sessionId } = React.useRef<string>(Date.now().toString());
const analyticsMeta = { sessionId };
const { title = null } = presentationProps ?? {};
const id = videos[0]?.id ?? null;
const analyticsMeta = { sessionId, title, id };

// ---------
// setup onEnd effect on unmount
Expand Down
20 changes: 14 additions & 6 deletions packages/apollos-ui-media-player/src/RNVideo.tsx
Expand Up @@ -9,7 +9,12 @@ import {
import Video from 'react-native-video';
import { styled, ActivityIndicator } from '@apollosproject/ui-kit';

import { useNowPlaying, usePlayerControls, useInternalPlayer } from './context';
import {
useNowPlaying,
usePlayerControls,
useInternalPlayer,
usePlayhead,
} from './context';
import { PictureMode } from './types';

import CoverImage from './CoverImage';
Expand Down Expand Up @@ -57,11 +62,12 @@ const RNVideoPresentation = ({
updatePlayhead,
} = useInternalPlayer();

const playheadContext = usePlayhead();
const playheadRef = React.useRef({
totalDuration: 1,
seekableDuration: 1,
playableDuration: 1,
elapsedTime: 0,
totalDuration: playheadContext.totalDuration,
seekableDuration: playheadContext.seekableDuration,
playableDuration: playheadContext.playableDuration,
elapsedTime: playheadContext.elapsedTime,
});

const handleProgressProp = React.useCallback(
Expand All @@ -82,7 +88,6 @@ const RNVideoPresentation = ({
playableDuration: playhead.playableDuration,
elapsedTime: playhead.currentTime,
};

playheadRef.current = newPlayhead;
// the default currentTime is often a very low value
// ex: 0.000009999999747378752
Expand All @@ -97,6 +102,9 @@ const RNVideoPresentation = ({
setShowLoading(false);
playheadRef.current = { ...playheadRef.current, totalDuration: duration };
updatePlayhead(playheadRef.current);
if (playheadRef.current.elapsedTime > 0.01) {
seek(playheadRef.current.elapsedTime);
}
};

const videoRef = React.useRef<VideoExpanded>(null);
Expand Down
6 changes: 6 additions & 0 deletions packages/apollos-ui-media-player/src/types.tsx
Expand Up @@ -89,3 +89,9 @@ export interface IInternalPlayer {
// Used by the internal VideoComponent to inform of playhead updates
updatePlayhead: React.Dispatch<React.SetStateAction<IPlayhead>>;
}

export interface IVideo {
id: string;
name?: string;
duration?: number;
}
3 changes: 3 additions & 0 deletions templates/mobile/package.json
Expand Up @@ -35,6 +35,9 @@
"./jest.setup.js"
]
},
"resolutions": {
"react-devtools-core": "4.24.3"
},
"dependencies": {
"@apollo/client": "3.3.20",
"@apollosproject/config": "link:../../packages/apollos-config",
Expand Down

0 comments on commit fbbaaea

Please sign in to comment.