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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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 (