diff --git a/assets/svg/full-screen-collapse.svg b/assets/svg/full-screen-collapse.svg new file mode 100644 index 000000000..4c1f37198 --- /dev/null +++ b/assets/svg/full-screen-collapse.svg @@ -0,0 +1,12 @@ + + + + Full-screen-collapse + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/assets/svg/full-screen.svg b/assets/svg/full-screen.svg new file mode 100644 index 000000000..5bf95cc20 --- /dev/null +++ b/assets/svg/full-screen.svg @@ -0,0 +1,12 @@ + + + + Full-screen + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/assets/svg/mute.svg b/assets/svg/mute.svg new file mode 100644 index 000000000..daf802e32 --- /dev/null +++ b/assets/svg/mute.svg @@ -0,0 +1,12 @@ + + + + Mute + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/assets/svg/replay.svg b/assets/svg/replay.svg new file mode 100644 index 000000000..e4acf608f --- /dev/null +++ b/assets/svg/replay.svg @@ -0,0 +1,12 @@ + + + + replay + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/assets/svg/unmute.svg b/assets/svg/unmute.svg new file mode 100644 index 000000000..49f355843 --- /dev/null +++ b/assets/svg/unmute.svg @@ -0,0 +1,12 @@ + + + + Un-Mute + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/package.json b/package.json index 6ae6bcd3a..26572c8a0 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "branch-sdk": "^2.22.1", "cluster": "^0.7.7", "crypto-js": "3.1.8", + "dashjs": "^2.4.1", "event-tracker": "git://github.com/reddit/event-tracker.git#28bc2dd9ce3b80540fdf189e1b003f8264cd0d08", "js-cookie": "^2.1.1", "json-stable-stringify": "^1.0.1", @@ -45,8 +46,8 @@ "raf": "^3.2.0", "raven": "^1.1.1", "raven-js": "^3.7.0", - "react": "15.0.2", - "react-dom": "15.0.2", + "react": "15.4.0", + "react-dom": "15.4.0", "react-motion": "0.4.4", "react-redux": "4.4.5", "redux": "3.5.2", @@ -66,7 +67,7 @@ "eslint-plugin-babel": "^3.2.0", "eslint-plugin-react": "^5.1.1", "mocha": "2.4.5", - "react-addons-test-utils": "15.0.1", + "react-addons-test-utils": "15.5.0", "sinon": "1.17.3", "sinon-chai": "2.8.0", "webpack-bundle-analyzer": "^2.2.1" diff --git a/src/apiClient/models/PostModel.js b/src/apiClient/models/PostModel.js index 147e36240..71efd31c8 100644 --- a/src/apiClient/models/PostModel.js +++ b/src/apiClient/models/PostModel.js @@ -79,6 +79,7 @@ export default class PostModel extends RedditModel { thirdPartyTracking2: T.string, thirdPartyTrackers: T.string, userReports: T.array, + videoPlaytime: T.number, // derived expandable: T.bool, diff --git a/src/app/actions/posts.js b/src/app/actions/posts.js index 9d0ef4689..e297f9e69 100644 --- a/src/app/actions/posts.js +++ b/src/app/actions/posts.js @@ -106,6 +106,18 @@ export const toggleHidePost = postId => async (dispatch, getState) => { } }; +export const updatePostPlaytime = (postId, newPlaytime) => async (dispatch, getState) => { + const state = getState(); + const post = state.posts[postId]; + + try { + const newPost = PostModel.fromJSON({ ...post.toJSON(), videoPlaytime: newPlaytime }); + dispatch(updatePlaying(newPost)); + } catch (e) { + console.error(e); + } +}; + export const TOGGLE_EDIT = 'POSTS__TOGGLE_EDIT'; export const toggleEdit = postId => ({ type: TOGGLE_EDIT, @@ -124,6 +136,13 @@ export const stopPlaying = postId => ({ thingId: postId, }); +//Video time must be kept current so that reports can be generated with video time +export const UPDATE_VIDEO_TIME = 'POSTS__UPDATE_VIDEO_TIME'; +export const updatePlaying = (post) => ({ + type: UPDATE_VIDEO_TIME, + post: post, +}); + export const UPDATING_SELF_TEXT = 'POSTS__UPDATING_SELF_TEXT'; export const updatingSelfText = postId => ({ type: UPDATING_SELF_TEXT, diff --git a/src/app/actions/reporting.js b/src/app/actions/reporting.js index 7009f3d0a..304c14739 100644 --- a/src/app/actions/reporting.js +++ b/src/app/actions/reporting.js @@ -23,7 +23,7 @@ export const report = thingId => async (dispatch, getState) => { const thing = modelFromThingId(thingId, state); const thingType = ModelTypes.thingType(thingId); const usesSubredditRules = SubredditRule.doRulesApplyToThingType(thingType); - + const props = { thingId, thingType, @@ -33,6 +33,11 @@ export const report = thingId => async (dispatch, getState) => { props.subredditName = thing.subreddit; } + //If video playtime exists (video was playing when report generated) submit it with the report + if (thing.videoPlaytime) { + props.videoPlaytime = thing.videoPlaytime; + } + dispatch({ type: REPORT, modalType: MODAL_TYPE, @@ -68,11 +73,15 @@ export const submit = report => async (dispatch, getState) => { const username = state.user.name; try { + const model = modelFromThingId(report.thingId, state); + report.reportTime = model.videoPlaytime; + const body = { // 'reason' is either the shortname of a rule, or a special keyword // The naming in the api is... it could be better. reason: report.ruleName, thing_id: report.thingId, + report_time: report.reportTime, api_type: 'json', }; @@ -90,7 +99,6 @@ export const submit = report => async (dispatch, getState) => { body, }); - const model = modelFromThingId(report.thingId, state); let moderatesSub = false; if (state.moderatingSubreddits && includes(state.moderatingSubreddits.names, model.subreddit)) { diff --git a/src/app/components/HTML5StreamPlayer/index.jsx b/src/app/components/HTML5StreamPlayer/index.jsx new file mode 100644 index 000000000..6034a0ceb --- /dev/null +++ b/src/app/components/HTML5StreamPlayer/index.jsx @@ -0,0 +1,506 @@ +import React from 'react'; +import dashjs from 'dashjs'; +import { debounce } from 'lodash'; +import './styles.less'; + +import PostModel from 'apiClient/models/PostModel'; + +const T = React.PropTypes; + +class HTML5StreamPlayer extends React.Component { + static propTypes = { + // ownProps + hlsSource: T.string.isRequired, + mpegDashSource: T.string.isRequired, + aspectRatioClassname: T.string.isRequired, + postData: T.instanceOf(PostModel), + onUpdatePostPlaytime: T.func.isRequired, + scrubberThumbSource: T.string.isRequired, + isGif: T.bool.isRequired, + }; + + constructor(props) { + super(props); + this.state = { + videoPaused: false, + videoScrollPaused: true, + videoMuted: true, + videoPosition: 0, + videoEnded: false, + videoFullScreen: false, + debounceFunc: null, + videoInView: true, + currentTime: '00:00', + totalTime: '00:00', + currentlyScrubbing: false, + scrubPosition: 0, + thumbPosition: 0, + mediaPlayer: null, + videoLoaded: false, + autoPlay: true, + }; + } + + getMobileOperatingSystem() { + const userAgent = navigator.userAgent || navigator.vendor || window.opera; + if (/android/i.test(userAgent)) { + return 'Android'; + } + // iOS detection from: http://stackoverflow.com/a/9039885/177710 + if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) { + return 'iOS'; + } + + return 'unknown'; + } + + isIOS() { + return (this.getMobileOperatingSystem() === 'iOS'); + } + + isAndroid() { + return (this.getMobileOperatingSystem() === 'Android'); + } + + isScrolledIntoView = () => { + if (!this.state.videoScrollPaused || this.state.videoFullScreen) { + return; + } + + const videoContainer = this.refs.HTML5StreamPlayerContainer; + + const elemTop = videoContainer.getBoundingClientRect().top; + const elemBottom = videoContainer.getBoundingClientRect().bottom; + + const totalVideoHeight = elemBottom - elemTop; + let videoHeight; + if (elemTop < 0) { + videoHeight = elemBottom; + } else if (elemBottom > window.innerHeight) { + videoHeight = innerHeight - elemTop; + } else { + videoHeight = totalVideoHeight; + } + + let videoIsInView = false; + if ((videoHeight / totalVideoHeight) > 0.5) { + videoIsInView = true; + } + + if (this.state.videoInView !== videoIsInView) { + const video = this.refs.HTML5StreamPlayerVideo; + if (videoIsInView) { + if (video.paused && !window.fullScreen) { + video.play(); + } + } else { + if (!video.paused) { + video.pause(); + } + } + + this.setState({videoInView: videoIsInView, videoScrollPaused: true}); + } + } + + secondsToMinutes(seconds) { + let minutes = Math.floor(seconds/60).toString(); + let seconds2 = Math.trunc(seconds%60).toString(); + + if (minutes.length === 1) { minutes = `0${minutes}`; } + if (seconds2.length === 1) { seconds2 = `0${seconds2}`; } + + return `${minutes}:${seconds2}`; + } + + videoDidLoad = () => { + if (this) { + this.setState({videoLoaded: true}); + } + } + + componentDidMount() { + //if non-hls compatible browser, initialize dashjs media player (dash.js handles this check automatically). + const video = this.refs.HTML5StreamPlayerVideo; + const player = dashjs.MediaPlayerFactory.create(video); + + document.addEventListener('webkitfullscreenchange', this.exitHandler, false); + document.addEventListener('mozfullscreenchange', this.exitHandler, false); + document.addEventListener('fullscreenchange', this.exitHandler, false); + document.addEventListener('MSFullscreenChange', this.exitHandler, false); + + video.addEventListener('canplay', this.videoDidLoad, false); + //draw initial buffer background (null video); + this.drawBufferBar(); + + const debounceFunc = debounce(this.isScrolledIntoView, 50); + window.addEventListener('scroll', debounceFunc); + + //store function handler for removal + this.setState({debounceFunc, mediaPlayer: player}); + + if (this.props.postData.videoPlaytime) { + video.currentTime = this.props.postData.videoPlaytime; + if (this.state.autoPlay && this.state.paused) { + this.playPauseVideo(); + } + } + } + + componentWillMount() { + //if video has a previous time position, prevent autoplay, this stops the video from continuing unintentionally on report modal open/close + if (this.props.postData.videoPlaytime) { + this.setState({autoPlay: false}); + } + } + + componentWillUnmount() { + const video = this.refs.HTML5StreamPlayerVideo; + video.removeEventListener('canplay', this.videoDidLoad, false); + window.removeEventListener('scroll', this.state.debounceFunc); + + document.removeEventListener('webkitfullscreenchange', this.exitHandler, false); + document.removeEventListener('mozfullscreenchange', this.exitHandler, false); + document.removeEventListener('fullscreenchange', this.exitHandler, false); + document.removeEventListener('MSFullscreenChange', this.exitHandler, false); + } + + playPauseVideo = () => { + const video = this.refs.HTML5StreamPlayerVideo; + + if (video.paused) { + video.play(); + this.setState({videoPaused: false, videoScrollPaused: true, videoEnded:false}); + } else if (this.props.isGif) { + //is gif + if (this.state.videoFullScreen) { + this.exitFullscreen(); + } else { + this.enterFullScreen(); + } + } else { + video.pause(); + this.setState({videoPaused: true, videoScrollPaused: false}); + } + } + + resetVideo = () => { + const video = this.refs.HTML5StreamPlayerVideo; + video.currentTime = 0.1; + } + + exitHandler = () => { + if (this.state.videoFullScreen === true) { + this.setState({videoFullScreen: false}); + this.exitFullscreen(); + } else { + this.setState({videoFullScreen: true}); + } + } + + exitFullscreen = () => { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } + } + + enterFullScreen = () => { + //Default to standard video controls in fullscreen for iOS + let video = this.refs.HTML5StreamPlayerVideo; + + if (this.isAndroid()) { + video = this.refs.HTML5StreamPlayerContainer; + } + + if (video.requestFullscreen) { + video.requestFullscreen(); + } else if (video.webkitEnterFullscreen) { + video.webkitEnterFullscreen(); + } else if (video.mozRequestFullScreen) { + video.mozRequestFullScreen(); // Firefox + } else if (video.webkitRequestFullscreen) { + video.webkitRequestFullscreen(); // Chrome and Safari + } + + if (this.state.videoMuted) { + this.muteVideo(); + } + } + + muteVideo = () => { + const video = this.refs.HTML5StreamPlayerVideo; + video.muted = !video.muted; + this.setState({videoMuted: video.muted}); + } + + renderMute() { + //if gif, no mute button + if (this.props.isGif) { + return; + } + + const video = this.refs.HTML5StreamPlayerVideo; + if ((video && video.muted) || this.state.videoMuted) { + return (); + } + + return (); + } + + renderPlaybackIcon() { + if (!this.state.videoLoaded) { + return null; + } + + const video = this.refs.HTML5StreamPlayerVideo; + + if (this.state.videoEnded && !this.props.isGif) { + return ( +
+
+ +
+
+ ); + } else if (video.paused) { + return ( +
+
+ +
+
+ ); + } + return null; + } + + setVideoPos = (event) => { + const video = this.refs.scrubberThumbnail; + const bufferBar = this.refs.scrubBuffer; + const value = event.target.value; + if (video) { + video.currentTime = ((video.duration/100) * value).toFixed(1); + } + this.setState({ + scrubPosition: value, + thumbPosition: ((bufferBar.clientWidth-16) * value/100 + 2), + }); //(bufferWidth - thumb width) //2 == border width + } + + drawBufferBar(video = null) { + + //no bufferbar for gifs + if (this.props.isGif) { + return; + } + + const bufferBar = this.refs.scrubBuffer; + const context = bufferBar.getContext('2d'); + + //Bufferbar height needs to be set to clientHeight on initial load to prevent blending glitches from canvas stretching (safari). + if (video === null) { + bufferBar.height = bufferBar.clientHeight; + } + + context.fillStyle = '#CCCCCA'; + context.fillRect(0, 0, bufferBar.width, bufferBar.height); + + if (video) { + context.fillStyle = '#939393'; + context.strokeStyle = '#939393'; + + const inc = bufferBar.width / video.duration; + + //draw buffering each update + for (let i = 0; i < video.buffered.length; i++) { + const startX = video.buffered.start(i) * inc; + const endX = video.buffered.end(i) * inc; + const width = endX - startX; + + context.fillRect(startX, 0, width, bufferBar.height); + context.stroke(); + } + + context.fillStyle = '#0DD3BB'; + context.strokeStyle = '#0DD3BB'; + context.fillRect(0, 0, video.currentTime * inc, bufferBar.height); + } + } + + updateTime = () => { + //Create buffer bar for data + const video = this.refs.HTML5StreamPlayerVideo; + this.drawBufferBar(video); + + if (video.currentTime && video.duration) { + let isVideoEnded = false; + if (video.currentTime >= video.duration) { + if (!this.props.isGif) { + isVideoEnded = true; + } + } + this.setState({ + videoPosition: ((video.currentTime/video.duration) * 100), + videoEnded:isVideoEnded, + currentTime: this.secondsToMinutes(video.currentTime), + totalTime: this.secondsToMinutes(video.duration), + }); + this.props.onUpdatePostPlaytime(video.currentTime); + } + } + + renderThumbnail() { + return ( +
+
+ +
+
+ ); + } + + scrubEnd = () => { + const video = this.refs.HTML5StreamPlayerVideo; + video.currentTime = (video.duration/100) * this.state.scrubPosition; + this.setState({currentlyScrubbing: false, videoPosition: this.state.scrubPosition}); + } + + scrubStart = () => { + this.setState({currentlyScrubbing: true}); + } + + render() { + return ( +
+
+
+ +
+ +
+
+ +
+ +
+ + { !this.props.isGif && +
+ +
+ } + +
+ +
+ + { !this.props.isGif && +
+
+
+ { this.state.totalTime } +
+ +
+ { this.state.currentTime } +
+ + + + + + + { this.renderThumbnail() } +
+
+ } +
+
+
+
+ ); + } + +} + +export default HTML5StreamPlayer; diff --git a/src/app/components/HTML5StreamPlayer/styles.less b/src/app/components/HTML5StreamPlayer/styles.less new file mode 100644 index 000000000..6c9dd3d6b --- /dev/null +++ b/src/app/components/HTML5StreamPlayer/styles.less @@ -0,0 +1,420 @@ +@import (reference) '~app/less/variables'; +@import (reference) '~app/less/mixins/importAll'; +@import (reference) '~app/less/themes/themeify'; +@import '~app/less/components/aspect-ratio'; + + +@track-color: transparent; +@thumb-color: #0DD3BB; + +@thumb-radius: 16px; +@thumb-height: 16px; +@thumb-width: 16px; +@thumb-shadow-size: 1px; +@thumb-shadow-blur: 1px; +@thumb-shadow-color: #111; +@thumb-border-width: 1px; +@thumb-border-color: white; + +//Must be 100% total +@track-width: 70%; +@track-margin-width: 15%; +//- +@track-button-spacing: 5%; + +@track-height: 4px; +@track-shadow-size: 2px; +@track-shadow-blur: 2px; +@track-shadow-color: #222; + +@track-radius: 100px; +@contrast: 5%; +@thumbnail-size: 70px; + +.shadow(@shadow-size,@shadow-blur,@shadow-color) { + box-shadow: @shadow-size @shadow-size @shadow-blur @shadow-color, 0px 0px @shadow-size lighten(@shadow-color,5%); +} + +.track() { + width: @track-width; + height: @track-height; + cursor: pointer; + animate: 0.2s; +} + +.thumb() { + .shadow(@thumb-shadow-size,@thumb-shadow-blur,@thumb-shadow-color); + border: @thumb-border-width solid @thumb-border-color; + height: @thumb-height; + width: @thumb-width; + border-radius: @thumb-radius; + background: @thumb-color; + cursor: pointer; + z-index: 999; +} + +.HTML5StreamPlayer { + box-sizing: border-box; + height: auto; + max-width: 100vw; + + &__video__fullscreen { + -webkit-video-playable-inline: true; + width: 100%; + height: 100%; + left: 50%; + margin: 0 auto; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + } + + &__video__regular { + width: 100%; + height: 100%; + position: absolute; + top: 0; + -webkit-video-playable-inline: true; + } + + &__video__regular::-webkit-media-controls-play-button { + display: none; + } + + &__video__regular::-webkit-media-controls-start-playback-button { + display: none!important; + -webkit-appearance: none; + } + + &__videoContainer { + position: relative; + max-width: 100%; + background-color: black; + } + + &__videoContainer__fullscreen { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + background: black; + } + + &__videoTrim:before { + z-index: -999; + pointer-events: none; + } + + &__controlPanel { + position: absolute; + top: 0; + width: 100%; + height: 100%; + } + + &__control { + outline: none; + + &:focus { + outline: none; + } + + &__play { + outline: none; + height: 100%; + width:100%; + } + + &__play:hover .HTML5StreamPlayer__playback-action-circle{ + opacity: 0.8; + background-color: #0DD3BB; + } + + &__bar { + position: absolute; + height: 45px; + left: 0; + bottom: 0; + max-width: 100vw; + width: 100vw; + padding-top: 20px; + padding-left: @track-button-spacing; + padding-right: @track-button-spacing; + background: -moz-linear-gradient(top, rgba(0,0,0,0) 0%, rgba(0,0,0,0.65) 100%); /* FF3.6-15 */ + background: -webkit-linear-gradient(top, rgba(0,0,0,0) 0%,rgba(0,0,0,0.65) 100%); /* Chrome10-25,Safari5.1-6 */ + background: linear-gradient(to bottom, rgba(0,0,0,0) 0%,rgba(0,0,0,0.65) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00000000', endColorstr='#a6000000',GradientType=0 ); /* IE6-9 */ + } + + &__fullscreen { + outline: none; + float: right; + margin-right: @track-button-spacing; + height: 25px; + width: 25px; + font-size: 25px; + } + + &__mute { + outline: none; + position: absolute; + left: 0; + height: 25px; + width: 25px; + font-size: 25px; + } + + &__barMargin { + margin-left: @track-margin-width; + margin-right: @track-margin-width; + } + + &__timeCurrent { + float: left; + width: 35px; + margin-left: -35px; + margin-top: -5px; + color: #0DD3BB; + font-size: 12px; + font-weight: bold; + text-align: left; + } + + &__scrubThumb { + height: @thumbnail-size; + width: @thumbnail-size; + position: absolute; + top: -85px; + background-color: black; + } + + &__thumbContainer { + position: relative; + width: 100%; + margin-left: -30px; + } + + &__scrubVideo { + height: @thumbnail-size; + width: @thumbnail-size; + } + + &__scrubThumbHidden { + display: none; + position: absolute; + } + + &__timeTotal { + float: right; + width: 35px; + margin-right: -35px; + margin-top: -5px; + color: #0DD3BB; + font-size: 12px; + font-weight: bold; + text-align: right; + } + + &__scrubberContainer { + position: relative; + justify-content: center; + align-items: center; + height: 25px; + width: auto; + margin-top: 8px; + margin-left: 35px; + margin-right: 35px; + } + + &__scrubBar { + height: @track-height; + width: @track-width; + border-radius: 100px; + position:absolute; top:0; left:0; + margin-left: @track-margin-width; + margin-right: @track-margin-width; + } + + &__scrubBar__buffer { + border-radius: @track-radius; + width: @track-width; + height: @track-height; + background-color: @track-color ; + pointer-events: none; + position:absolute; top:0; left:0; z-index:0; + margin-left: @track-margin-width; + margin-right: @track-margin-width; + } + + &__scrubBar { + position: absolute; + -webkit-appearance: none; + width: @track-width; + + &:focus { + outline: none; + } + + &::-webkit-slider-runnable-track { + .track(); + background: @track-color; + border-radius: @track-radius; + } + + &::-webkit-slider-thumb { + .thumb(); + -webkit-appearance: none; + margin-top: (@track-height / 2) - (@thumb-height / 2); + } + + &:focus::-webkit-slider-runnable-track { + background: lighten(@track-color, @contrast); + } + + &::-moz-range-track { + .track(); + background: @track-color; + border-radius: @track-radius; + } + &::-moz-range-thumb { + .thumb(); + } + + &::-ms-track { + .track(); + background: transparent; + border-color: transparent; + border-width: @thumb-width 0; + color: transparent; + } + + &::-ms-fill-lower { + background: darken(@track-color, @contrast); + border-radius: @track-radius*2; + } + &::-ms-fill-upper { + background: @track-color; + border-radius: @track-radius*2; + } + &::-ms-thumb { + .thumb(); + } + &:focus::-ms-fill-lower { + background: @track-color; + } + &:focus::-ms-fill-upper { + background: lighten(@track-color, @contrast); + } + + } + + } + + &__playback-full-screen { + outline: none; + width: 25px; + height: 25px; + &.icon-full-screen { + outline: none; + width: 25px; + height: 25px; + color: #0DD3BB; + } + } + + &__playback-full-screen-collapse { + outline: none; + width: 25px; + height: 25px; + &.icon-full-screen-collapse { + outline: none; + width: 25px; + height: 25px; + color: #0DD3BB; + } + } + + &__playback-mute { + outline: none; + width: 25px; + height: 25px; + &.icon-mute { + outline: none; + width: 25px; + height: 25px; + color: #0DD3BB; + } + } + + + &__playback-unmute { + outline: none; + width: 25px; + height: 25px; + &.icon-unmute { + outline: none; + width: 25px; + height: 25px; + color: #0DD3BB; + } + } + + &__replay-icon-container { + padding-top:0.35em; + padding-left:0.35em; + } + + &__play-icon-container { + padding-top:10px; + } + + &__playback-action-circle { + opacity: 0.9; + background-color: #0DD3BB; + + position: absolute; + top: 50%; + left: 50%; + text-align: center; + + &:hover { + opacity: 0.8; + background-color: #0DD3BB; + } + + .icon-size(@icon-size) { + width: @icon-size; + height: @icon-size; + + @half-icon-size: floor(@icon-size / 2); + border-radius: @half-icon-size; + margin-top: -@half-icon-size; + margin-left: -@half-icon-size; + } + + &.regular { + .icon-size(48px); + } + + &.compact { + .icon-size(28px); + } + } + + &__playback-action-icon { + // some of the icons are a little off center... + &.icon-play_triangle { + margin-left: 0.3em; + font-size: 28px; + line-height: 28px; + } + &.icon-replay { + font-size: 48px; + width: 48px; + height: 48px; + line-height: 48px; + } + } + +} diff --git a/src/app/components/OutboundLink/index.jsx b/src/app/components/OutboundLink/index.jsx index bc36ed3c8..756ae8ca8 100644 --- a/src/app/components/OutboundLink/index.jsx +++ b/src/app/components/OutboundLink/index.jsx @@ -54,8 +54,7 @@ const resetOriginalURL = ($target, href) => { function OutboundLink(props) { const { outboundLink, userId, href, onClick } = props; // get all of the props we want to pass to standard react components (styles, className, etc) - const linkProps = omit(props, 'outboundLink'); - + const linkProps = omit(props, ['outboundLink','userId','dispatch']); const clickHandler = onClick || (() => null); if (!outboundLink) { diff --git a/src/app/components/Post/PostContent/index.jsx b/src/app/components/Post/PostContent/index.jsx index 996e9689e..d811293fa 100644 --- a/src/app/components/Post/PostContent/index.jsx +++ b/src/app/components/Post/PostContent/index.jsx @@ -12,6 +12,8 @@ import OutboundLink from 'app/components/OutboundLink'; import { LISTING_CLICK_TYPES } from 'app/constants'; +import HTML5StreamPlayer from 'app/components/HTML5StreamPlayer'; + import { isPostNSFW, cleanPostDomain, @@ -54,6 +56,7 @@ PostContent.propTypes = { editPending: T.bool, onToggleEdit: T.func.isRequired, onUpdateSelftext: T.func.isRequired, + onUpdatePostPlaytime: T.func.isRequired, forceHTTPS: T.bool.isRequired, isDomainExternal: T.bool.isRequired, renderMediaFullbleed: T.bool.isRequired, @@ -215,6 +218,9 @@ function buildMediaContent(post, linkDescriptor, props) { } e.preventDefault(); + if (isNSFW || isSpoiler) { + props.toggleShowNSFW(); + } togglePlaying(); }; return buildImagePreview(previewImage, sourceURL, linkDescriptor, @@ -242,25 +248,45 @@ function buildMediaContent(post, linkDescriptor, props) { function buildImagePreview(previewImage, imageURL, linkDescriptor, callback, needsObfuscating, playableType, props) { - const html5sources = gifToHTML5Sources(imageURL, previewImage.url); - const { single, isPlaying } = props; - if (isPlaying && html5sources) { - const { width, height } = previewImage; - const aspectRatio = getAspectRatio(single, width, height); + const { single, isPlaying, post } = props; + + if (isPlaying || !needsObfuscating) { + //locally hosted video + if (post.media && post.media.reddit_video) { + const { width, height } = previewImage; + const aspectRatio = getAspectRatio(single, width, height); - if (html5sources.iframe) { - return renderIframe(html5sources.iframe, aspectRatio); + const generatedSrc = { + dash: post.media.reddit_video.dash_url, + hls: post.media.reddit_video.hls_url, + scrubberThumbSource: post.media.reddit_video.scrubber_media_url, + isGif: post.media.reddit_video.is_gif, + width: previewImage.width, + height: previewImage.height, + }; + + return renderVideo(generatedSrc, previewImage, aspectRatio, props); } - const generatedSrc = { - webm: html5sources.webm, - mp4: html5sources.mp4, - width: previewImage.width, - height: previewImage.height, - }; + const html5sources = gifToHTML5Sources(imageURL, previewImage.url); + if (html5sources) { + const { width, height } = previewImage; + const aspectRatio = getAspectRatio(single, width, height); - return renderVideo(generatedSrc, html5sources.poster, aspectRatio); + if (html5sources.iframe) { + return renderIframe(html5sources.iframe, aspectRatio); + } + + const generatedSrc = { + webm: html5sources.webm, + mp4: html5sources.mp4, + width: previewImage.width, + height: previewImage.height, + }; + + return renderVideo(generatedSrc, html5sources.poster, aspectRatio, props); + } } return renderImage(previewImage, imageURL, linkDescriptor, callback, @@ -393,7 +419,31 @@ function renderIframe(src, aspectRatio) { ); } -function renderVideo(videoSpec, posterImage, aspectRatio) { +function renderVideo(videoSpec, posterImage, aspectRatio, props) { + const { post, onUpdatePostPlaytime } = props; + + if (videoSpec.hls || videoSpec.dash) { + //video limited to 16:9 as specced, will be letterboxed if different reservation. + let aspectRatio; + if ((videoSpec.width / videoSpec.height) < (16 / 9)) { + aspectRatio = getAspectRatio(false, 16, 9); + } else { + aspectRatio = getAspectRatio(false, videoSpec.width, videoSpec.height); + } + + return ( + + ); + } + return (