Skip to content

Commit

Permalink
feat(live): adding live ui preset and functionality (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dvir Hazout committed Aug 31, 2017
1 parent 7abb9b0 commit 81d9919
Show file tree
Hide file tree
Showing 17 changed files with 296 additions and 5 deletions.
6 changes: 6 additions & 0 deletions src/components/engine-connector/engine-connector.js
Expand Up @@ -41,11 +41,17 @@ class EngineConnector extends BaseComponent {
this.props.updateCurrentTime(this.player.currentTime);
});

this.player.addEventListener(this.player.Event.DURATION_CHANGE, () => {
this.props.updateDuration(this.player.duration);
});

this.player.addEventListener(this.player.Event.LOADED_METADATA, () => {
this.props.updateDuration(this.player.duration);
this.props.updateMuted(this.player.muted);
this.props.updateMetadataLoadingStatus(true);
this.props.updatePlayerPoster(this.player.poster);
this.props.updateIsLive(this.player.isLive());
this.props.updateIsDvr(this.player.isDvr());
});

this.player.addEventListener(this.player.Event.VOLUME_CHANGE, () => {
Expand Down
30 changes: 30 additions & 0 deletions src/components/live-tag/_live-tag.scss
@@ -0,0 +1,30 @@
.live-tag {
color: $live-color;
font-size: 14px;
font-weight: bold;
letter-spacing: 1px;
line-height: 19px;
border: 2px solid $live-color;
border-radius: 4px;
text-transform: uppercase;
text-align: center;
display: inline-block;
padding: 0 3px 0 5px;
margin: 5px 23px;
cursor: default;

&.non-live-playhead {
background-color: rgba(255,255,255,0.2);
border: none;
color: #fff;
line-height: 23px;
padding: 0 5px 0 7px;
cursor: pointer;
}
}

@media screen and (max-width: 480px) {
.live-tag {
margin-left: 0;
}
}
1 change: 1 addition & 0 deletions src/components/live-tag/index.js
@@ -0,0 +1 @@
export default from './live-tag';
80 changes: 80 additions & 0 deletions src/components/live-tag/live-tag.js
@@ -0,0 +1,80 @@
//@flow
import { h } from 'preact';
import { connect } from 'preact-redux';
import BaseComponent from '../base';

/**
* mapping state to props
* @param {*} state - redux store state
* @returns {Object} - mapped state to this component
*/
const mapStateToProps = state => ({
isLive: state.engine.isLive,
isDvr: state.engine.isDvr,
currentTime: state.engine.currentTime,
duration: state.engine.duration
});

@connect(mapStateToProps)
/**
* LiveTag component
*
* @class LiveTag
* @example <LiveTag player={this.player} />
* @extends {BaseComponent}
*/
class LiveTag extends BaseComponent {

/**
* Creates an instance of LiveTag.
* @param {Object} obj obj
* @memberof LiveTag
*/
constructor(obj: Object) {
super({name: 'LiveTag', player: obj.player});
}

/**
* returns a boolean to detect if player is on live edge with buffer of 1 second
*
* @returns {boolean} - is player on live edge
* @memberof LiveTag
*/
isOnLiveEdge(): boolean {
return (this.props.currentTime >= this.props.duration - 1);
}

/**
* click handler to live tag
* if not on live edge, seeking to live edge and if paused, call play method
*
* @returns {void}
* @memberof LiveTag
*/
onClick(): void {
if (!this.isOnLiveEdge()) {
this.player.seekToLiveEdge();
if (this.player.paused) {
this.player.play();
}
}
}

/**
* render live tag component
*
* @param {*} props - component props
* @returns {React$Element} component element
* @memberof LiveTag
*/
render(props: any): React$Element<any> {
var tagStyleClass = 'live-tag';
if (props.isDvr && !this.isOnLiveEdge()) tagStyleClass += ' non-live-playhead';

return (
<div className={tagStyleClass} onClick={() => this.onClick()}>Live</div>
)
}
}

export default LiveTag;
Expand Up @@ -94,6 +94,11 @@ class PrePlaybackPlayOverlay extends BaseComponent {
*/
handleClick(): void {
this.player.play();

if (this.props.prePlayback) {
this.props.updatePrePlayback(false);
this.props.removePlayerClass('pre-playback');
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/components/seekbar-live-playback-container/index.js
@@ -0,0 +1 @@
export default from './seekbar-live-playback-container';
@@ -0,0 +1,85 @@
//@flow
import { h } from 'preact';
import { connect } from 'preact-redux';
import { bindActions } from '../../utils/bind-actions';
import { actions } from '../../reducers/seekbar';
import BaseComponent from '../base';
import SeekBarControl from '../seekbar';

/**
* mapping state to props
* @param {*} state - redux store state
* @returns {Object} - mapped state to this component
*/
const mapStateToProps = state => ({
currentTime: state.seekbar.currentTime,
duration: state.engine.duration,
isDraggingActive: state.seekbar.draggingActive,
isMobile: state.shell.isMobile,
poster: state.engine.poster,
isDvr: state.engine.isDvr
});

@connect(mapStateToProps, bindActions(actions))
/**
* SeekBarLivePlaybackContainer component
*
* @class SeekBarLivePlaybackContainer
* @example <SeekBarLivePlaybackContainer player={this.player} />
* @extends {BaseComponent}
*/
class SeekBarLivePlaybackContainer extends BaseComponent {

/**
* Creates an instance of SeekBarLivePlaybackContainer.
* @param {Object} obj obj
* @memberof SeekBarLivePlaybackContainer
*/
constructor(obj: Object) {
super({name: 'SeekBarLivePlaybackContainer', player: obj.player});
}

/**
* after component mounted, listen to time update event and if dragging not active,
* update the current time in the store
*
* @returns {void}
* @memberof SeekBarLivePlaybackContainer
*/
componentDidMount() {
this.player.addEventListener(this.player.Event.TIME_UPDATE, () => {
if (!this.props.isDraggingActive) {
this.props.updateCurrentTime(this.player.currentTime);
}
});
}

/**
* render component
*
* @param {*} props - component props
* @returns {React$Element} - component element
* @memberof SeekBarLivePlaybackContainer
*/
render(props: any) {
if (!props.isDvr) return undefined;
return (
<SeekBarControl
playerElement={this.player.getView().parentElement}
showTimeBubble={this.props.showTimeBubble}
changeCurrentTime={time => this.player.currentTime = time}
playerPoster={this.props.poster}
updateSeekbarDraggingStatus={data => this.props.updateSeekbarDraggingStatus(data)}
updateCurrentTime={data => this.props.updateCurrentTime(data)}

isDvr={this.props.isDvr}
currentTime={this.props.currentTime}
duration={this.props.duration}
isDraggingActive={this.props.isDraggingActive}
isMobile={this.props.isMobile}
/>
)
}

}
export default SeekBarLivePlaybackContainer;
6 changes: 6 additions & 0 deletions src/components/seekbar/_seekbar.scss
Expand Up @@ -28,6 +28,12 @@
}
}

&.live {
.progress-bar .progress {
background-color: $live-color;
}
}

.progress-bar {
height: $progress-bar-height;
background-color: rgba(255,255,255,0.3);
Expand Down
4 changes: 3 additions & 1 deletion src/components/seekbar/seekbar.js
Expand Up @@ -286,7 +286,8 @@ class SeekBarControl extends Component {
renderTimeBubble(): React$Element<any> | void {
if (!this.props.showTimeBubble || this.props.isMobile) return undefined;
var timeBubbleStyle = `left: ${this.getTimeBubbleOffset()}px`;
return <div className='time-preview' style={timeBubbleStyle} ref={c => this._timeBubbleElement=c}>{ toHHMMSS(this.state.virtualTime)}</div>
var timeBubbleValue = this.props.isDvr ? '-' + toHHMMSS(this.props.duration - this.state.virtualTime) : toHHMMSS(this.state.virtualTime);
return <div className='time-preview' style={timeBubbleStyle} ref={c => this._timeBubbleElement=c}>{timeBubbleValue}</div>
}

/**
Expand All @@ -301,6 +302,7 @@ class SeekBarControl extends Component {
var progressWidth = `${props.currentTime / props.duration * 100}%`;
var seekbarStyleClass = `seek-bar`;
if (props.adBreak) seekbarStyleClass += ' ad-break';
if (props.isDvr) seekbarStyleClass += ' live';
if (props.isMobile) seekbarStyleClass += ' hover';

return (
Expand Down
5 changes: 4 additions & 1 deletion src/player-gui.js
Expand Up @@ -10,7 +10,10 @@ import { connect } from 'preact-redux';
const mapStateToProps = state => ({
state: {
shell: state.shell,
engine: { adBreak: state.engine.adBreak }
engine: {
adBreak: state.engine.adBreak,
isLive: state.engine.isLive
}
}
});

Expand Down
1 change: 1 addition & 0 deletions src/playkit-js-ui.js
Expand Up @@ -5,6 +5,7 @@ export {h} from 'preact';
// ui presets
export {default as playbackUI} from './ui-presets/playback';
export {default as adsUI} from './ui-presets/ads';
export {default as liveUI} from './ui-presets/live';

// components
export {OverlayPlay} from './components/overlay-play';
Expand Down
22 changes: 20 additions & 2 deletions src/reducers/engine.js
Expand Up @@ -17,7 +17,9 @@ export const types = {
UPDATE_AD_SKIP_TIME_OFFSET: 'engine/UPDATE_AD_SKIP_TIME_OFFSET',
UPDATE_AD_SKIPPABLE_STATE: 'engine/UPDATE_AD_SKIPPABLE_STATE',
UPDATE_AD_URL: 'engine/UPDATE_AD_URL',
UPDATE_PLAYER_POSTER: 'engine/UPDATE_PLATER_POSTER'
UPDATE_PLAYER_POSTER: 'engine/UPDATE_PLATER_POSTER',
UPDATE_IS_LIVE: 'engine/UPDATE_IS_LIVE',
UPDATE_IS_DVR: 'engine/UPDATE_IS_DVR'
}

export const initialState = {
Expand All @@ -40,6 +42,8 @@ export const initialState = {
adIsPlaying: false,
adSkipTimeOffset: 0,
adSkippableState: false,
isLive: false,
isDvr: false,
adProgress: {
currentTime: 0,
duration: 0
Expand Down Expand Up @@ -157,6 +161,18 @@ export default (state: Object = initialState, action: Object) => {
poster: action.poster
}

case types.UPDATE_IS_LIVE:
return {
...state,
isLive: action.isLive
}

case types.UPDATE_IS_DVR:
return {
...state,
isDvr: action.isDvr
}

default:
return state;
}
Expand All @@ -180,5 +196,7 @@ export const actions = {
updateAdSkipTimeOffset: (adSkipTimeOffset: boolean) => ({ type: types.UPDATE_AD_SKIP_TIME_OFFSET, adSkipTimeOffset }),
updateAdSkippableState: (adSkippableState: boolean) => ({ type: types.UPDATE_AD_SKIPPABLE_STATE, adSkippableState }),
updateAdClickUrl: (adUrl: string) => ({ type: types.UPDATE_AD_URL, adUrl }),
updatePlayerPoster: (poster: string) => ({ type: types.UPDATE_PLAYER_POSTER, poster })
updatePlayerPoster: (poster: string) => ({ type: types.UPDATE_PLAYER_POSTER, poster }),
updateIsLive: (isLive: boolean) => ({ type: types.UPDATE_IS_LIVE, isLive }),
updateIsDvr: (isDvr: boolean) => ({ type: types.UPDATE_IS_DVR, isDvr })
}
1 change: 1 addition & 0 deletions src/styles/_variables.scss
Expand Up @@ -6,6 +6,7 @@ $grayscale4: #ccc;
$success-color: #009444;
$danger-color: #db1f26;
$ads-color: #F9A71B;
$live-color: #DA1F26;
$font-family: 'Lato', sans-serif;
$progress-bar-height: 4px;
$progress-bar-border-radius: $progress-bar-height / 2;
Expand Down
1 change: 1 addition & 0 deletions src/styles/style.scss
Expand Up @@ -29,3 +29,4 @@
@import '../components/overlay-play/overlay-play';
@import '../components/pre-playback-play-overlay/pre-playback-play-overlay';
@import '../components/ad-skip/ad-skip';
@import '../components/live-tag/live-tag';
2 changes: 2 additions & 0 deletions src/ui-manager.js
Expand Up @@ -17,6 +17,7 @@ import PlayerGUI from './player-gui';
// ui presets
import adsUI from './ui-presets/ads';
import playbackUI from './ui-presets/playback';
import liveUI from './ui-presets/live';

import './styles/style.scss';

Expand Down Expand Up @@ -54,6 +55,7 @@ class UIManager {
buildDefaultUI(): void {
const uis = [
{ template: props => adsUI(props), condition: state => state.engine.adBreak },
{ template: props => liveUI(props), condition: state => state.engine.isLive },
{ template: props => playbackUI(props) }
];
this._buildUI(uis);
Expand Down

0 comments on commit 81d9919

Please sign in to comment.