├── .babelrc.js ├── .gitignore ├── .prettierignore ├── README.MD ├── commitlint.config.js ├── dist ├── es │ ├── components │ │ ├── Controls.js │ │ ├── Dragger.js │ │ ├── FilePicker.js │ │ ├── Icon.js │ │ ├── Player.js │ │ ├── Status.js │ │ └── Trimmer.js │ ├── index.js │ ├── libs │ │ ├── WebVideo.js │ │ ├── formatSeconds.js │ │ ├── preloadWebVideo.js │ │ └── utils.js │ ├── style.js │ └── styles │ │ ├── controls.scss │ │ ├── dragger.scss │ │ ├── file-picker.scss │ │ ├── icon.scss │ │ ├── main-container.scss │ │ ├── player.scss │ │ ├── status.scss │ │ └── trimmer.scss ├── index.js └── style.css ├── docs ├── build │ └── bundle.65bace37.js └── index.html ├── examples ├── BASE.MD └── theme.css ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── components │ ├── Controls.js │ ├── Dragger.js │ ├── FilePicker.js │ ├── Icon.js │ ├── Player.js │ ├── Status.js │ └── Trimmer.js ├── icons │ ├── download.svg │ ├── music.svg │ ├── pause.svg │ ├── play.svg │ ├── replay.svg │ └── spin.svg ├── index.js ├── libs │ ├── WebVideo.js │ ├── formatSeconds.js │ ├── preloadWebVideo.js │ └── utils.js ├── style.js └── styles │ ├── controls.scss │ ├── dragger.scss │ ├── file-picker.scss │ ├── icon.scss │ ├── main-container.scss │ ├── player.scss │ ├── status.scss │ └── trimmer.scss ├── styleguide.config.js ├── styleguide └── setup.js ├── testSetup.js └── yarn.lock /.babelrc.js: -------------------------------------------------------------------------------- 1 | // https://babeljs.io/docs/en/configuration 2 | const presets = ["@babel/preset-react"]; 3 | if (process.env["BABEL_ENV"] === "es") { 4 | presets.unshift(["@babel/preset-env", { modules: false }]); 5 | } else { 6 | presets.unshift("@babel/preset-env"); 7 | } 8 | 9 | const plugins = [ 10 | "@babel/plugin-proposal-export-default-from", 11 | "@babel/plugin-proposal-class-properties", 12 | "@babel/plugin-proposal-logical-assignment-operators", 13 | ["@babel/plugin-proposal-optional-chaining", { loose: false }], 14 | ["@babel/plugin-proposal-pipeline-operator", { proposal: "minimal" }], 15 | ["@babel/plugin-proposal-nullish-coalescing-operator", { loose: false }], 16 | "@babel/plugin-proposal-do-expressions", 17 | "add-module-exports" 18 | ]; 19 | 20 | module.exports = { 21 | presets, 22 | plugins, 23 | env: { 24 | test: { 25 | presets, 26 | plugins: [ 27 | ...plugins, 28 | "@babel/plugin-transform-runtime", 29 | "dynamic-import-node" 30 | ] 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | .env.test 68 | 69 | # parcel-bundler cache (https://parceljs.org/) 70 | .cache 71 | 72 | # next.js build output 73 | .next 74 | 75 | # nuxt.js build output 76 | .nuxt 77 | 78 | # vuepress build output 79 | .vuepress/dist 80 | 81 | # Serverless directories 82 | .serverless/ 83 | 84 | # FuseBox cache 85 | .fusebox/ 86 | 87 | # DynamoDB Local files 88 | .dynamodb/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | docs/ 3 | node_modules/ -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # react-video-trimmer 2 | 3 | Amazing React component for manipulating video length. 4 | 5 | With the aid of FFMPEG in the browser, it get easier to do amazing things with 6 | media content. 7 | 8 | ## Demo 9 | 10 | [See Demo](https://limistah.github.io/react-video-trimmer/) 11 | 12 | ## Installation 13 | 14 | ```bash 15 | npm install --save react-video-trimmer 16 | ``` 17 | 18 | or: 19 | 20 | ```bash 21 | yarn add react-video-trimmer 22 | ``` 23 | 24 | ## Usage 25 | 26 | **NOTE:** Do import the styles from `react-video-trimmer/dist/style.css` 27 | 28 | ```js 29 | import ReactVideoTrimmer from "react-video-trimmer"; 30 |
31 | 32 |
; 33 | ``` 34 | 35 | ## Props 36 | 37 | ### timeRange: Number 38 | 39 | Start and end value in seconds the trimmer should restrict to.ti 40 | 41 | ```js static 42 | import React from "react"; 43 | import ReactVideoTrimmer from "react-video-trimmer"; 44 | import "react-video-trimmer/dist/style.css"; 45 | 46 | const Trimmer = () => { 47 | return ( 48 |
49 | 50 |
51 | ); 52 | }; 53 | ``` 54 | 55 | ### onVideoEncode: function 56 | 57 | Handler that receives the video encoding once it has been encoded 58 | 59 | ```js static 60 | import React from "react"; 61 | import ReactVideoTrimmer from "react-video-trimmer"; 62 | import "react-video-trimmer/dist/style.css"; 63 | 64 | const Trimmer = () => { 65 | const handleVideoEncode = React.useCallback(result => { 66 | console.log("Encoding Result:", result); 67 | }); 68 | return ( 69 |
70 | 74 |
75 | ); 76 | }; 77 | ``` 78 | 79 | ### loadingFFMPEGText: string 80 | 81 | A text to tell users that FFMPEG is being loaded in the background. 82 | 83 | Default: _Please wait..._ 84 | 85 | ```js static 86 | import React from "react"; 87 | import ReactVideoTrimmer from "react-video-trimmer"; 88 | import "react-video-trimmer/dist/style.css"; 89 | 90 | const Trimmer = () => { 91 | const handleVideoEncode = React.useCallback(result => { 92 | console.log("Encoding Result:", result); 93 | }); 94 | return ( 95 |
96 | 101 |
102 | ); 103 | }; 104 | ``` 105 | 106 | ## React Ref 107 | 108 | Pass a ref to access all the static methods of ReactVideoTrimmer methods 109 | 110 | ```js static 111 | import React from "react"; 112 | import ReactVideoTrimmer from "react-video-trimmer"; 113 | import "react-video-trimmer/dist/style.css"; 114 | 115 | const Trimmer = () => { 116 | const trimmerRef = React.useRef(); 117 | return ( 118 |
119 | 120 |
121 | ); 122 | }; 123 | ``` 124 | 125 | ## Methods 126 | 127 | ### handleFFMPEGStdout: void 128 | 129 | A listener to [ffmpeg-webworker](https://www.npmjs.com/package/ffmpeg-webworker) 130 | `FFMPEGStdout` event 131 | 132 | ### handleFFMPEGReady: void 133 | 134 | A listener to [ffmpeg-webworker](https://www.npmjs.com/package/ffmpeg-webworker) 135 | `FFMPEGReady` event 136 | 137 | ### handleFFMPEGFileReceived: void 138 | 139 | A listener to [ffmpeg-webworker](https://www.npmjs.com/package/ffmpeg-webworker) 140 | `FFMPEGFileReceived` event 141 | 142 | ### handleFFMPEGDone: void 143 | 144 | A listener to [ffmpeg-webworker](https://www.npmjs.com/package/ffmpeg-webworker) 145 | `FFMPEGDone` event 146 | 147 | > Converts the returned result into a `Blob`, before updating the video player 148 | 149 | ### decodeVideoFile: void 150 | 151 | ##### params 152 | 153 | - **file: Blob** A Blob/File with type matching `video/*` 154 | - **doneCB: function** Called after the decode action is completed 155 | 156 | ### handleFileSelected: void 157 | 158 | Called when a valid video file is selected, in turn calls `decodeVideoFile` for 159 | proper handling 160 | 161 | ##### params 162 | 163 | - **file: Blob** A Blob/File with type matching `video/*` 164 | 165 | ### handleEncodeVideo: void 166 | 167 | Called when a valid video file is selected, in turn calls `decodeVideoFile` for 168 | proper handling 169 | 170 | ##### params 171 | 172 | - **file: Blob** A Blob/File with type matching `video/*` 173 | 174 | ### handleDownloadVideo: void 175 | 176 | Handler for the Download button `onClick` event. Downloads the converted video 177 | file 178 | 179 | ##### params 180 | 181 | - **encodedVideo: Blob** Encoded video data converted to `Blob` 182 | 183 | ## Preloading ffmpeg 184 | 185 | ffmpeg.js will not load until the component is in scope. To override this, a 186 | `preloadWebVideo` field has been included to make ffmpeg load ahead of this 187 | component mount period. 188 | 189 | ```js static 190 | import React from "react"; 191 | import { preloadWebVideo } from "react-video-trimmer"; 192 | 193 | // It is a function, no other process is required 194 | preloadWebVideo(); 195 | ``` 196 | 197 | ## License 198 | 199 | MIT 200 | 201 | ## Credit 202 | 203 | This library uses the work of the guys at 204 | [ffmpeg-webworker](https://www.npmjs.com/package/ffmpeg-webworker) 205 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/label-has-associated-control */ 2 | // eslint-disable-next-line quotes 3 | module.exports = { extends: ["@commitlint/config-angular"] }; 4 | -------------------------------------------------------------------------------- /dist/es/components/Controls.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Icon from "./Icon"; 3 | 4 | var Controls = function Controls(_ref) { 5 | var onPlayPauseClick = _ref.onPlayPauseClick, 6 | playing = _ref.playing, 7 | onReselectFile = _ref.onReselectFile, 8 | processing = _ref.processing, 9 | onEncode = _ref.onEncode, 10 | showEncodeBtn = _ref.showEncodeBtn, 11 | canDownload = _ref.canDownload, 12 | onDownload = _ref.onDownload; 13 | return React.createElement("div", { 14 | className: "rvt-controls-cont" 15 | }, React.createElement("a", { 16 | className: "rvt-controller-item", 17 | title: "Pause", 18 | onClick: onPlayPauseClick 19 | }, React.createElement(Icon, { 20 | name: playing ? "pause" : "play" 21 | })), React.createElement("a", { 22 | className: "rvt-controller-item", 23 | title: "Select File", 24 | onClick: onReselectFile 25 | }, React.createElement(Icon, { 26 | name: "music" 27 | })), showEncodeBtn && React.createElement("div", { 28 | className: "rvt-controller-dropdown rvt-controller-list-wrap" 29 | }, canDownload ? React.createElement("a", { 30 | className: "rvt-controller-item", 31 | onClick: onDownload 32 | }, React.createElement(Icon, { 33 | name: "download" 34 | })) : React.createElement("a", { 35 | className: "rvt-controller-item", 36 | onClick: onEncode 37 | }, React.createElement(Icon, { 38 | name: processing ? "spin" : "replay" 39 | })))); 40 | }; 41 | 42 | export default Controls; -------------------------------------------------------------------------------- /dist/es/components/Dragger.js: -------------------------------------------------------------------------------- 1 | function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 2 | 3 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 4 | 5 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 6 | 7 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 8 | 9 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 10 | 11 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 12 | 13 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 14 | 15 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 16 | 17 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 18 | 19 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 20 | 21 | import React, { PureComponent } from "react"; 22 | import PropTypes from "prop-types"; 23 | 24 | var Dragger = 25 | /*#__PURE__*/ 26 | function (_PureComponent) { 27 | _inherits(Dragger, _PureComponent); 28 | 29 | function Dragger() { 30 | var _getPrototypeOf2; 31 | 32 | var _this; 33 | 34 | _classCallCheck(this, Dragger); 35 | 36 | for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { 37 | args[_key] = arguments[_key]; 38 | } 39 | 40 | _this = _possibleConstructorReturn(this, (_getPrototypeOf2 = _getPrototypeOf(Dragger)).call.apply(_getPrototypeOf2, [this].concat(args))); 41 | 42 | _defineProperty(_assertThisInitialized(_this), "_screenX", null); 43 | 44 | _defineProperty(_assertThisInitialized(_this), "_screenY", null); 45 | 46 | _defineProperty(_assertThisInitialized(_this), "_ox", null); 47 | 48 | _defineProperty(_assertThisInitialized(_this), "_oy", null); 49 | 50 | _defineProperty(_assertThisInitialized(_this), "handleMouseDown", function (e) { 51 | _this._screenX = e.screenX; 52 | _this._screenY = e.screenY; 53 | _this._ox = _this.props.x; 54 | _this._oy = _this.props.y; 55 | window.addEventListener("mousemove", _this.handleMouseMove, false); 56 | window.addEventListener("mouseup", _this.handleMouseUp, false); 57 | }); 58 | 59 | _defineProperty(_assertThisInitialized(_this), "handleMouseMove", function (e) { 60 | _this.props.onDrag({ 61 | x: e.screenX - _this._screenX + _this._ox, 62 | y: e.screenY - _this._screenY + _this._oy 63 | }); 64 | }); 65 | 66 | _defineProperty(_assertThisInitialized(_this), "handleMouseUp", function () { 67 | window.removeEventListener("mousemove", _this.handleMouseMove); 68 | window.removeEventListener("mouseup", _this.handleMouseUp); 69 | 70 | var handler = _this.props.onDragStop || function () {}; 71 | 72 | handler(); 73 | }); 74 | 75 | return _this; 76 | } 77 | 78 | _createClass(Dragger, [{ 79 | key: "render", 80 | value: function render() { 81 | return React.createElement("div", { 82 | className: "rvt-dragger " + this.props.className || "", 83 | onMouseDown: this.handleMouseDown, 84 | style: { 85 | left: this.props.x + "px", 86 | top: this.props.y + "px" 87 | } 88 | }, this.props.children); 89 | } 90 | }]); 91 | 92 | return Dragger; 93 | }(PureComponent); 94 | 95 | _defineProperty(Dragger, "defaultProps", { 96 | onDrag: function onDrag() {}, 97 | x: 0, 98 | y: 0 99 | }); 100 | 101 | _defineProperty(Dragger, "propTypes", { 102 | x: PropTypes.number, 103 | y: PropTypes.number, 104 | onDrag: PropTypes.func, 105 | className: PropTypes.string, 106 | children: PropTypes.element 107 | }); 108 | 109 | export { Dragger as default }; -------------------------------------------------------------------------------- /dist/es/components/FilePicker.js: -------------------------------------------------------------------------------- 1 | function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } 2 | 3 | import React, { useCallback, useRef } from "react"; 4 | import { isVideo, noop } from "../libs/utils"; 5 | import Icon from "./Icon"; 6 | import Dropzone from "react-dropzone"; 7 | 8 | function FilePicker(props) { 9 | var onDrop = useCallback(function (acceptedFiles) { 10 | if (acceptedFiles.length) { 11 | var video = acceptedFiles[0]; 12 | var handler = props.onFileSelected || noop; 13 | handler(video); 14 | } 15 | }, []); 16 | var MAX_SIZE = props.maxSize || 10000024; 17 | var MIN_SIZE = props.minSize || 0; // const handleFileChange = useCallback(e => { 18 | // if (e.target.files.length) { 19 | // const video = e.target.files[0]; 20 | // if (isVideo(video)) { 21 | // const handler = props.onFileSelected || noop; 22 | // handler(video); 23 | // } else { 24 | // return alert("Unsupported File Type"); 25 | // } 26 | // } 27 | // }); 28 | 29 | var toMB = function toMB(_byte) { 30 | return Math.round(_byte / 1000000); 31 | }; 32 | 33 | return React.createElement(Dropzone, { 34 | onDrop: onDrop, 35 | maxSize: MAX_SIZE, 36 | minSize: MIN_SIZE, 37 | accept: "video/*" 38 | }, function (_ref) { 39 | var getRootProps = _ref.getRootProps, 40 | getInputProps = _ref.getInputProps, 41 | isDragActive = _ref.isDragActive; 42 | return React.createElement("div", _extends({}, getRootProps(), { 43 | className: "rvt-file-picker" 44 | }), React.createElement(Icon, { 45 | name: "music" 46 | }), React.createElement("input", getInputProps()), isDragActive ? React.createElement("p", null, "Drop the video here ...") : React.createElement(React.Fragment, null, React.createElement("p", null, "Drag 'n' drop a video here, or click to select one"), React.createElement("p", null, React.createElement("small", null, "(", toMB(MIN_SIZE), " - ", toMB(MAX_SIZE), "MB)")))); 47 | }); 48 | } 49 | 50 | export default FilePicker; -------------------------------------------------------------------------------- /dist/es/components/Icon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | var Download = function Download(_ref) { 5 | var className = _ref.className; 6 | return React.createElement("svg", { 7 | className: className, 8 | viewBox: "0 0 24 24", 9 | xmlns: "http://www.w3.org/2000/svg" 10 | }, React.createElement("path", { 11 | d: "M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" 12 | }), React.createElement("path", { 13 | d: "M0 0h24v24H0z", 14 | fill: "none" 15 | })); 16 | }; 17 | 18 | var Music = function Music(_ref2) { 19 | var className = _ref2.className; 20 | return React.createElement("svg", { 21 | className: className, 22 | viewBox: "0 0 24 24", 23 | xmlns: "http://www.w3.org/2000/svg" 24 | }, React.createElement("path", { 25 | d: "M0 0h24v24H0z", 26 | fill: "none" 27 | }), React.createElement("path", { 28 | d: "M15 6H3v2h12V6zm0 4H3v2h12v-2zM3 16h8v-2H3v2zM17 6v8.18c-.31-.11-.65-.18-1-.18-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3V8h3V6h-5z" 29 | })); 30 | }; 31 | 32 | var Play = function Play(_ref3) { 33 | var className = _ref3.className; 34 | return React.createElement("svg", { 35 | className: className, 36 | height: "24", 37 | viewBox: "0 0 24 24", 38 | width: "24", 39 | xmlns: "http://www.w3.org/2000/svg" 40 | }, React.createElement("path", { 41 | d: "M0 0h24v24H0z", 42 | fill: "none" 43 | }), React.createElement("path", { 44 | d: "M10 16.5l6-4.5-6-4.5v9zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" 45 | })); 46 | }; 47 | 48 | var Pause = function Pause(_ref4) { 49 | var className = _ref4.className; 50 | return React.createElement("svg", { 51 | className: className, 52 | height: "24", 53 | viewBox: "0 0 24 24", 54 | width: "24", 55 | xmlns: "http://www.w3.org/2000/svg" 56 | }, React.createElement("path", { 57 | d: "M0 0h24v24H0z", 58 | fill: "none" 59 | }), React.createElement("path", { 60 | d: "M9 16h2V8H9v8zm3-14C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm1-4h2V8h-2v8z" 61 | })); 62 | }; 63 | 64 | var Replay = function Replay(_ref5) { 65 | var className = _ref5.className; 66 | return React.createElement("svg", { 67 | className: className, 68 | viewBox: "0 0 24 24", 69 | xmlns: "http://www.w3.org/2000/svg" 70 | }, React.createElement("path", { 71 | d: "M0 0h24v24H0z", 72 | fill: "none" 73 | }), React.createElement("path", { 74 | d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" 75 | })); 76 | }; 77 | 78 | var Spin = function Spin(_ref6) { 79 | var className = _ref6.className; 80 | return React.createElement("svg", { 81 | className: className, 82 | xmlns: "http://www.w3.org/2000/svg", 83 | viewBox: "0 0 42 42" 84 | }, React.createElement("path", { 85 | d: "M21 37c-4.3 0-8.3-1.7-11.3-4.7S5 25.3 5 21c0-3 .8-6 2.5-8.5C9 10 11.2 8 13.8 6.7l1.3 2.7c-2.1 1.1-3.9 2.7-5.2 4.7-1.3 2.1-2 4.5-2 6.9 0 7.2 5.8 13 13 13s13-5.8 13-13c0-2.5-.7-4.9-2-6.9s-3.1-3.6-5.2-4.7L28 6.7c2.8 1.3 5 3.3 6.5 5.8C36.2 15 37 18 37 21c0 4.3-1.7 8.3-4.7 11.3S25.3 37 21 37z" 86 | })); 87 | }; 88 | 89 | var Icon = function Icon(props) { 90 | var El = Download; 91 | 92 | switch (props.name) { 93 | case "music": 94 | El = Music; 95 | break; 96 | 97 | case "play": 98 | El = Play; 99 | break; 100 | 101 | case "pause": 102 | El = Pause; 103 | break; 104 | 105 | case "replay": 106 | El = Replay; 107 | break; 108 | 109 | case "spin": 110 | El = Spin; 111 | break; 112 | 113 | default: 114 | El = Download; 115 | break; 116 | } 117 | 118 | return React.createElement(El, { 119 | className: "rvt-icon rat-icon-".concat(props.name, " ").concat(props.className).trim() 120 | }); 121 | }; 122 | 123 | Icon.propTypes = { 124 | name: PropTypes.string 125 | }; 126 | export default Icon; -------------------------------------------------------------------------------- /dist/es/components/Player.js: -------------------------------------------------------------------------------- 1 | function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 2 | 3 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 4 | 5 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 6 | 7 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 8 | 9 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 10 | 11 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 12 | 13 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 14 | 15 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 16 | 17 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 18 | 19 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 20 | 21 | import React from "react"; 22 | import ReactPlayer from "react-player"; 23 | import { formatSeconds, noop, leftZero } from "../libs/utils"; 24 | 25 | var Player = 26 | /*#__PURE__*/ 27 | function (_React$Component) { 28 | _inherits(Player, _React$Component); 29 | 30 | function Player() { 31 | var _getPrototypeOf2; 32 | 33 | var _this; 34 | 35 | _classCallCheck(this, Player); 36 | 37 | for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { 38 | args[_key] = arguments[_key]; 39 | } 40 | 41 | _this = _possibleConstructorReturn(this, (_getPrototypeOf2 = _getPrototypeOf(Player)).call.apply(_getPrototypeOf2, [this].concat(args))); 42 | 43 | _defineProperty(_assertThisInitialized(_this), "state", { 44 | playing: _this.props.playVideo || false 45 | }); 46 | 47 | _defineProperty(_assertThisInitialized(_this), "handlePlayerProgress", function (data) { 48 | if (data.loaded) { 49 | var playedSeconds = data.playedSeconds; 50 | var startTimeRange = _this.props.timeRange.start; 51 | var endTimeRange = _this.props.timeRange.end; 52 | var playedSecondsIsLowerThanStartTime = playedSeconds <= startTimeRange; 53 | var playedSecondsIsGreaterThanEndTime = playedSeconds >= endTimeRange; 54 | 55 | if (playedSecondsIsLowerThanStartTime) { 56 | _this.player.seekTo(startTimeRange, "seconds"); 57 | } 58 | 59 | if (playedSecondsIsGreaterThanEndTime) { 60 | _this.player.seekTo(startTimeRange, "seconds"); // this.setState({ playing: false }); 61 | 62 | } 63 | 64 | var handler = _this.props.onPlayerProgress || noop; 65 | handler(playedSeconds); 66 | } 67 | }); 68 | 69 | _defineProperty(_assertThisInitialized(_this), "handleOnPause", function () { 70 | var handler = _this.props.onPlayerPause || noop; 71 | handler(); 72 | }); 73 | 74 | _defineProperty(_assertThisInitialized(_this), "handleOnPlay", function () { 75 | var handler = _this.props.onPlayerPlay || noop; 76 | handler(); 77 | }); 78 | 79 | return _this; 80 | } 81 | 82 | _createClass(Player, [{ 83 | key: "componentWillReceiveProps", 84 | value: function componentWillReceiveProps(newProps) { 85 | var newTimeRange = newProps.timeRange; 86 | var oldTimeRange = this.props.timeRange; 87 | var canSeek = oldTimeRange && newTimeRange.start !== oldTimeRange.start || !oldTimeRange && newTimeRange.start > 0; 88 | 89 | if (canSeek) { 90 | this.setState({ 91 | playing: false 92 | }); 93 | this.player.seekTo(newTimeRange.start, "seconds"); 94 | } 95 | 96 | if (newProps.playVideo !== this.props.playVideo) { 97 | this.setState({ 98 | playing: newProps.playVideo 99 | }); 100 | } 101 | } 102 | }, { 103 | key: "displaySeconds", 104 | value: function displaySeconds(seconds) { 105 | return seconds.toFixed(2) + "s"; 106 | } 107 | }, { 108 | key: "render", 109 | value: function render() { 110 | var _this2 = this, 111 | _React$createElement; 112 | 113 | var _this$props$timeRange = this.props.timeRange, 114 | start = _this$props$timeRange.start, 115 | end = _this$props$timeRange.end; 116 | return React.createElement("div", { 117 | className: "rvt-player-cont", 118 | onContextMenu: function onContextMenu() {} 119 | }, React.createElement(ReactPlayer, (_React$createElement = { 120 | onPlay: this.handleOnPause 121 | }, _defineProperty(_React$createElement, "onPlay", this.handleOnPlay), _defineProperty(_React$createElement, "onProgress", this.handlePlayerProgress), _defineProperty(_React$createElement, "url", this.props.src), _defineProperty(_React$createElement, "ref", function ref(el) { 122 | return _this2.player = el; 123 | }), _defineProperty(_React$createElement, "playing", this.state.playing), _defineProperty(_React$createElement, "style", { 124 | margin: "0 auto" 125 | }), _React$createElement)), React.createElement("div", { 126 | className: "rvt-player-time-range-cont" 127 | }, React.createElement("span", { 128 | className: "rvt-player-time-range" 129 | }, "From: ", React.createElement("strong", null, this.displaySeconds(start))), React.createElement("span", { 130 | className: "rvt-player-time-range" 131 | }, "To: ", React.createElement("strong", null, this.displaySeconds(end))), React.createElement("span", { 132 | className: "rvt-player-time-range" 133 | }, "Selected ", React.createElement("strong", null, this.displaySeconds(end - start)), " of", " ", React.createElement("strong", null, this.displaySeconds(this.props.timeLimit)), " allowed"))); 134 | } 135 | }]); 136 | 137 | return Player; 138 | }(React.Component); 139 | 140 | export default Player; -------------------------------------------------------------------------------- /dist/es/components/Status.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | var Status = function Status(_ref) { 4 | var children = _ref.children; 5 | return React.createElement("div", { 6 | className: "rvt-status" 7 | }, children); 8 | }; 9 | 10 | export default Status; -------------------------------------------------------------------------------- /dist/es/components/Trimmer.js: -------------------------------------------------------------------------------- 1 | function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 2 | 3 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 4 | 5 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 6 | 7 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 8 | 9 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 10 | 11 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 12 | 13 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 14 | 15 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 16 | 17 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 18 | 19 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 20 | 21 | import React, { PureComponent } from "react"; 22 | import Dragger from "./Dragger"; 23 | import { noop, formatSeconds, leftZero } from "../libs/utils"; 24 | 25 | var TrimmerOverLay = function TrimmerOverLay(props) { 26 | return React.createElement("div", { 27 | className: "rvt-trimmer", 28 | style: { 29 | width: props.width, 30 | left: props.left, 31 | right: props.right 32 | } 33 | }); 34 | }; 35 | 36 | var TimeStamp = function TimeStamp(props) { 37 | var formated = formatSeconds(props.time); 38 | return React.createElement("div", { 39 | className: "rvt-player-cursor-current" 40 | }, React.createElement("span", { 41 | className: "rvt-player-num" 42 | }, formated[0]), "'", React.createElement("span", { 43 | className: "rvt-player-num" 44 | }, formated[1]), !props.noMicroSeconds && React.createElement(React.Fragment, null, ".", React.createElement("span", { 45 | className: "rvt-player-num" 46 | }, leftZero(formated[2], 2)))); 47 | }; 48 | 49 | var Trimmer = 50 | /*#__PURE__*/ 51 | function (_PureComponent) { 52 | _inherits(Trimmer, _PureComponent); 53 | 54 | function Trimmer() { 55 | var _getPrototypeOf2; 56 | 57 | var _this; 58 | 59 | _classCallCheck(this, Trimmer); 60 | 61 | for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { 62 | args[_key] = arguments[_key]; 63 | } 64 | 65 | _this = _possibleConstructorReturn(this, (_getPrototypeOf2 = _getPrototypeOf(Trimmer)).call.apply(_getPrototypeOf2, [this].concat(args))); 66 | 67 | _defineProperty(_assertThisInitialized(_this), "pos2Time", function (pos) { 68 | return pos / _this.props.widthDurationRatio; 69 | }); 70 | 71 | _defineProperty(_assertThisInitialized(_this), "time2pos", function (time) { 72 | return time * _this.props.widthDurationRatio; 73 | }); 74 | 75 | _defineProperty(_assertThisInitialized(_this), "keepInRange", function (x) { 76 | var containerWidth = _this.props.containerWidth; 77 | 78 | if (x < 0) { 79 | return 0; 80 | } 81 | 82 | if (x > containerWidth) { 83 | return containerWidth; 84 | } 85 | 86 | return x; 87 | }); 88 | 89 | _defineProperty(_assertThisInitialized(_this), "withinTimeLimit", function (time) { 90 | var isDragEnd = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; 91 | var timeLimit = _this.props.timeLimit; 92 | var startTime = _this.props.startTime; 93 | var endTime = time; 94 | 95 | if (!isDragEnd) { 96 | startTime = time; 97 | endTime = _this.props.endTime; 98 | } 99 | 100 | var duration = _this.props.duration; 101 | var timeTillEnd = duration - endTime; 102 | var currentRange = duration - startTime - timeTillEnd; 103 | return timeLimit ? currentRange <= timeLimit : true; 104 | }); 105 | 106 | _defineProperty(_assertThisInitialized(_this), "withinTimeRange", function (time) { 107 | var isDragEnd = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; 108 | var timeRange = _this.props.timeRangeLimit; 109 | var interval = time - _this.props.startTime; 110 | 111 | if (!isDragEnd) { 112 | interval = _this.props.endTime - time; 113 | } 114 | 115 | return timeRange ? interval >= timeRange : true; 116 | }); 117 | 118 | _defineProperty(_assertThisInitialized(_this), "handleDragStart", function (pos) { 119 | var pos2Time = _this.pos2Time(_this.keepInRange(pos.x)); 120 | 121 | var time = pos2Time; 122 | var currentTime = _this.props.currentTime; 123 | 124 | var currentTimeIsWithinRange = _this.withinTimeRange(time, false); 125 | 126 | var currentTimeIsWithinLimit = _this.withinTimeLimit(time, false); 127 | 128 | if (time >= currentTime || !currentTimeIsWithinRange || !currentTimeIsWithinLimit) { 129 | time = _this.props.startTime; 130 | 131 | var handler = _this.props.onPausePlayer || function () {}; 132 | 133 | handler(); 134 | } 135 | 136 | _this.props.onStartTimeChange(time); 137 | }); 138 | 139 | _defineProperty(_assertThisInitialized(_this), "handleDragEnd", function (pos) { 140 | var pos2Time = _this.pos2Time(_this.keepInRange(pos.x)); 141 | 142 | var time = pos2Time; 143 | var endTime = _this.props.endTime; 144 | var currentTime = _this.props.currentTime; 145 | 146 | var currentTimeIsWithinRange = _this.withinTimeRange(time); 147 | 148 | var currentTimeIsWithinLimit = _this.withinTimeLimit(time); 149 | 150 | if (currentTime >= time || !currentTimeIsWithinRange || !currentTimeIsWithinLimit) { 151 | time = _this.props.endTime; 152 | 153 | var handler = _this.props.onPausePlayer || function () {}; 154 | 155 | handler(); 156 | } 157 | 158 | _this.props.onEndTimeChange(time); 159 | }); 160 | 161 | _defineProperty(_assertThisInitialized(_this), "handleDragStop", function () { 162 | var handler = _this.props.onGetData || noop; 163 | handler({ 164 | start: _this.props.startTime, 165 | end: _this.props.endTime 166 | }); 167 | }); 168 | 169 | _defineProperty(_assertThisInitialized(_this), "getTrimmerWidth", function (width) { 170 | return _this.props.containerWidth - width; 171 | }); 172 | 173 | return _this; 174 | } 175 | 176 | _createClass(Trimmer, [{ 177 | key: "render", 178 | value: function render() { 179 | var start = this.time2pos(this.props.startTime); 180 | var end = this.time2pos(this.props.endTime); 181 | var current = this.time2pos(this.props.currentTime); 182 | return React.createElement(React.Fragment, null, React.createElement(TrimmerOverLay, { 183 | left: 0, 184 | width: start 185 | }), React.createElement(Dragger, { 186 | x: start, 187 | onDrag: this.handleDragStart, 188 | onDragStop: this.handleDragStop 189 | }, React.createElement(TimeStamp, { 190 | time: this.props.startTime 191 | })), React.createElement(Dragger, { 192 | x: current, 193 | onDrag: function onDrag() {}, 194 | onDragStop: function onDragStop() {} 195 | }, React.createElement(TimeStamp, { 196 | noMicroSeconds: true, 197 | time: this.props.currentTime 198 | })), React.createElement(Dragger, { 199 | x: end, 200 | onDrag: this.handleDragEnd, 201 | onDragStop: this.handleDragStop 202 | }, React.createElement(TimeStamp, { 203 | time: this.props.endTime 204 | })), React.createElement(TrimmerOverLay, { 205 | right: 0, 206 | width: this.getTrimmerWidth(end) 207 | })); 208 | } 209 | }]); 210 | 211 | return Trimmer; 212 | }(PureComponent); 213 | 214 | export var VideoTrimmer = 215 | /*#__PURE__*/ 216 | function (_PureComponent2) { 217 | _inherits(VideoTrimmer, _PureComponent2); 218 | 219 | function VideoTrimmer() { 220 | var _getPrototypeOf3; 221 | 222 | var _this2; 223 | 224 | _classCallCheck(this, VideoTrimmer); 225 | 226 | for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 227 | args[_key2] = arguments[_key2]; 228 | } 229 | 230 | _this2 = _possibleConstructorReturn(this, (_getPrototypeOf3 = _getPrototypeOf(VideoTrimmer)).call.apply(_getPrototypeOf3, [this].concat(args))); 231 | 232 | _defineProperty(_assertThisInitialized(_this2), "state", { 233 | start: 0, 234 | end: 0 235 | }); 236 | 237 | _defineProperty(_assertThisInitialized(_this2), "handleStartTimeChange", function (time) { 238 | _this2.setState({ 239 | start: time 240 | }); 241 | }); 242 | 243 | _defineProperty(_assertThisInitialized(_this2), "handleGetTrimData", function () { 244 | var trimmerHandler = _this2.props.onTrim || noop; 245 | setTimeout(function () { 246 | return trimmerHandler({ 247 | start: _this2.state.start || _this2.props.timeRange.start, 248 | end: _this2.state.end || _this2.props.timeRange.end 249 | }); 250 | }, 200); 251 | }); 252 | 253 | _defineProperty(_assertThisInitialized(_this2), "handleEndTimeChange", function (time) { 254 | _this2.setState({ 255 | end: time 256 | }); 257 | }); 258 | 259 | return _this2; 260 | } 261 | 262 | _createClass(VideoTrimmer, [{ 263 | key: "render", 264 | value: function render() { 265 | var _this3 = this; 266 | 267 | return React.createElement("div", { 268 | className: "rvt-trimmer-cont", 269 | ref: function ref(e) { 270 | return _this3.containerRef = e; 271 | } 272 | }, this.props.showTrimmer && React.createElement(Trimmer, { 273 | timeLimit: this.props.timeLimit, 274 | onStartTimeChange: this.handleStartTimeChange, 275 | onEndTimeChange: this.handleEndTimeChange, 276 | widthDurationRatio: this.widthDurationRatio, 277 | containerWidth: this.containerWidth, 278 | startTime: this.state.start || this.props.timeRange.start, 279 | endTime: this.state.end || this.props.timeRange.end, 280 | currentTime: this.props.currentTime, 281 | duration: this.props.duration, 282 | onGetData: this.handleGetTrimData, 283 | onPausePlayer: this.onPausePlayer 284 | })); 285 | } 286 | }, { 287 | key: "widthDurationRatio", 288 | get: function get() { 289 | return this.containerWidth / this.props.duration; 290 | } 291 | }, { 292 | key: "containerWidth", 293 | get: function get() { 294 | return this.containerRef.getBoundingClientRect().width; 295 | } 296 | }]); 297 | 298 | return VideoTrimmer; 299 | }(PureComponent); 300 | export default VideoTrimmer; -------------------------------------------------------------------------------- /dist/es/index.js: -------------------------------------------------------------------------------- 1 | function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 2 | 3 | function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } 4 | 5 | function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } 6 | 7 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 8 | 9 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 10 | 11 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 12 | 13 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 14 | 15 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 16 | 17 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 18 | 19 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 20 | 21 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 22 | 23 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 24 | 25 | import React from "react"; 26 | import FilePicker from "./components/FilePicker"; 27 | import Status from "./components/Status"; 28 | import Player from "./components/Player"; 29 | import Controls from "./components/Controls"; 30 | import Trimmer from "./components/Trimmer"; 31 | import WebVideo from "./libs/WebVideo"; 32 | import webVideoLoader from "./libs/preloadWebVideo"; 33 | import Icon from "./components/Icon"; 34 | import { noop, arrayBufferToBlob, readBlobURL, download } from "./libs/utils"; 35 | import "./style.js"; 36 | import PropTypes from "prop-types"; 37 | 38 | var ReactVideoTrimmer = 39 | /*#__PURE__*/ 40 | function (_React$PureComponent) { 41 | _inherits(ReactVideoTrimmer, _React$PureComponent); 42 | 43 | /** 44 | * @type {WebVideo} 45 | */ 46 | function ReactVideoTrimmer(props) { 47 | var _this2 = this; 48 | 49 | var _this; 50 | 51 | _classCallCheck(this, ReactVideoTrimmer); 52 | 53 | _this = _possibleConstructorReturn(this, _getPrototypeOf(ReactVideoTrimmer).call(this, props)); 54 | 55 | _defineProperty(_assertThisInitialized(_this), "webVideo", webVideoLoader({})); 56 | 57 | _defineProperty(_assertThisInitialized(_this), "handleFFMPEGStdout", function (msg) {// console.log(msg); 58 | }); 59 | 60 | _defineProperty(_assertThisInitialized(_this), "handleFFMPEGReady", function () { 61 | // console.log("FFMPEG is Ready"); 62 | _this.setState({ 63 | ffmpegReady: true 64 | }); 65 | }); 66 | 67 | _defineProperty(_assertThisInitialized(_this), "handleFFMPEGFileReceived", function () {// console.log("FFMPEG Received File"); 68 | }); 69 | 70 | _defineProperty(_assertThisInitialized(_this), "handleFFMPEGDone", function (result) { 71 | _this.setState({ 72 | timeRange: { 73 | start: 0, 74 | end: _this.state.timeRange.end 75 | } 76 | }); 77 | 78 | var videoBlob = arrayBufferToBlob(result[0].data); 79 | setTimeout(function () { 80 | _this.decodeVideoFile(videoBlob, function () { 81 | var handler = _this.props.onVideoEncode || noop; 82 | handler(result); 83 | 84 | _this.setState({ 85 | encoding: false, 86 | encoded: true, 87 | encodedVideo: videoBlob 88 | }); 89 | }); 90 | }, 300); 91 | }); 92 | 93 | _defineProperty(_assertThisInitialized(_this), "defaultState", { 94 | decoding: false, 95 | encoding: false, 96 | encoded: false, 97 | playVideo: false, 98 | videoDataURL: "", 99 | videoFrames: [], 100 | isDecoding: false, 101 | timeRange: { 102 | start: 5, 103 | end: _this.props.timeLimit || 15 104 | }, 105 | encodedVideo: null, 106 | playedSeconds: 0, 107 | ffmpegReady: false 108 | }); 109 | 110 | _defineProperty(_assertThisInitialized(_this), "state", _this.defaultState); 111 | 112 | _defineProperty(_assertThisInitialized(_this), "updateVideoDataURL", function (dataURL) { 113 | return _this.setState({ 114 | videoDataURL: dataURL 115 | }); 116 | }); 117 | 118 | _defineProperty(_assertThisInitialized(_this), "updateVideoFrames", function (frames) { 119 | return _this.setState({ 120 | videoFrames: frames 121 | }); 122 | }); 123 | 124 | _defineProperty(_assertThisInitialized(_this), "updateIsDecoding", function (state) { 125 | return _this.setState({ 126 | updateIsDecoding: state 127 | }); 128 | }); 129 | 130 | _defineProperty(_assertThisInitialized(_this), "updateVideoDuration", function (duration) { 131 | return _this.setState({ 132 | updateVideoDuration: duration 133 | }); 134 | }); 135 | 136 | _defineProperty(_assertThisInitialized(_this), "decodeVideoFile", function (file) { 137 | var doneCB = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop; 138 | 139 | _this.setState({ 140 | decoding: true 141 | }); 142 | 143 | var webVideo = _this.webVideo; 144 | webVideo.videoFile = file; 145 | webVideo.decode(file).then(function (_ref) { 146 | var blob = _ref.blob, 147 | arrayBuffer = _ref.arrayBuffer, 148 | dataURL = _ref.dataURL; 149 | 150 | _this.updateVideoDataURL(dataURL); 151 | 152 | var timeRangeStart = _this.state.timeRange.start; 153 | var duration = _this.webVideo.videoData.duration; 154 | var timeLimit = timeRangeStart + (_this.props.timeLimit || 10); 155 | var timeRangeEnd = duration > timeLimit ? timeLimit : duration; 156 | 157 | _this.setState({ 158 | timeRange: { 159 | start: timeRangeStart, 160 | end: timeRangeEnd 161 | }, 162 | playedSeconds: (timeRangeEnd - timeRangeStart) / 2 + timeRangeStart 163 | }); 164 | 165 | _this.setState({ 166 | decoding: false 167 | }); 168 | 169 | doneCB(); 170 | })["catch"](function (e) { 171 | return console.log(e); 172 | }); 173 | }); 174 | 175 | _defineProperty(_assertThisInitialized(_this), "handleFileSelected", function (file) { 176 | _this.decodeVideoFile(file); 177 | }); 178 | 179 | _defineProperty(_assertThisInitialized(_this), "handleVideoTrim", function (time) { 180 | _this.setState({ 181 | timeRange: time 182 | }); 183 | }); 184 | 185 | _defineProperty(_assertThisInitialized(_this), "handleEncodeVideo", function (timeRange) { 186 | _this.setState({ 187 | encoding: true, 188 | videoDataURL: "", 189 | playVideo: false 190 | }); 191 | 192 | var timeDifference = timeRange.end - timeRange.start; // console.log(timeRange); 193 | 194 | _this.webVideo.trimVideo(timeRange.start, timeDifference); 195 | }); 196 | 197 | _defineProperty(_assertThisInitialized(_this), "handlePlayPauseVideo", function () { 198 | var playVideo = _this.state.playVideo; 199 | 200 | _this.setState({ 201 | playVideo: !playVideo 202 | }); 203 | }); 204 | 205 | _defineProperty(_assertThisInitialized(_this), "handlePlayerPause", function () { 206 | // console.log("pause video"); 207 | _this.setState({ 208 | playVideo: false 209 | }); 210 | }); 211 | 212 | _defineProperty(_assertThisInitialized(_this), "handlePlayerPlay", function () { 213 | _this.setState({ 214 | playVideo: true 215 | }); 216 | }); 217 | 218 | _defineProperty(_assertThisInitialized(_this), "handlePlayerProgress", function (seconds) { 219 | if (_this.state.playVideo) { 220 | _this.setState({ 221 | playedSeconds: seconds 222 | }); 223 | } 224 | }); 225 | 226 | _defineProperty(_assertThisInitialized(_this), "handleReselectFile", function () { 227 | _this.setState(_objectSpread({}, _this.defaultState, { 228 | ffmpegReady: true 229 | })); 230 | }); 231 | 232 | _defineProperty(_assertThisInitialized(_this), "VideoPlayerWithTrimmer", function (_ref2) { 233 | var showTrimmer = _ref2.showTrimmer; 234 | var _this$state = _this.state, 235 | decoding = _this$state.decoding, 236 | encoding = _this$state.encoding, 237 | encoded = _this$state.encoded, 238 | videoDataURL = _this$state.videoDataURL; 239 | return React.createElement(React.Fragment, null, !decoding && !encoding && videoDataURL && React.createElement(Player, { 240 | src: _this.state.videoDataURL, 241 | timeRange: _this.state.timeRange, 242 | timeLimit: _this.props.timeLimit, 243 | playVideo: _this.state.playVideo, 244 | onPlayerPlay: _this.handlePlayerPlay, 245 | onPlayerPause: _this.handlePlayerPause, 246 | onPlayerProgress: _this.handlePlayerProgress 247 | }), showTrimmer && React.createElement(Trimmer, { 248 | onPausePlayer: _this.handlePlayerPause, 249 | showTrimmer: _this.state.videoDataURL, 250 | duration: _this.webVideo.videoData.duration, 251 | onTrim: _this.handleVideoTrim, 252 | timeLimit: _this.props.timeLimit, 253 | timeRangeLimit: _this.props.timeRange, 254 | timeRange: _this.state.timeRange, 255 | currentTime: _this.state.playedSeconds 256 | }), !decoding && !encoding && videoDataURL && React.createElement(Controls, { 257 | onDownload: function onDownload() { 258 | return _this.handleDownloadVideo(_this.state.encodedVideo); 259 | }, 260 | canDownload: encoded, 261 | showEncodeBtn: _this.props.showEncodeBtn, 262 | onReselectFile: _this.handleReselectFile, 263 | onEncode: function onEncode() { 264 | return _this.handleEncodeVideo(_this.state.timeRange); 265 | }, 266 | onPlayPauseClick: _this.handlePlayPauseVideo, 267 | processing: encoding, 268 | playing: _this.state.playVideo 269 | })); 270 | }); 271 | 272 | _defineProperty(_assertThisInitialized(_this), "handleDownloadVideo", function (encodedVideo) { 273 | var blobURL = readBlobURL(encodedVideo); 274 | download(blobURL, "trimmed.mp4"); 275 | }); 276 | 277 | _defineProperty(_assertThisInitialized(_this), "VideoPlayerNoTrimmer", function () { 278 | return React.createElement(_this2.VideoPlayerWithTrimmer, null); 279 | }); 280 | 281 | _this.webVideo.on("processingFile", function () { 282 | return _this.updateIsDecoding(true); 283 | }); 284 | 285 | _this.webVideo.on("processedFile", function () { 286 | return _this.updateIsDecoding(false); 287 | }); 288 | 289 | _this.webVideo.on("FFMPEGStdout", _this.handleFFMPEGStdout); 290 | 291 | _this.webVideo.on("FFMPEGReady", _this.handleFFMPEGReady); 292 | 293 | _this.webVideo.on("FFMPEGFileReceived", _this.handleFFMPEGFileReceived); 294 | 295 | _this.webVideo.on("FFMPEGDone", _this.handleFFMPEGDone); 296 | 297 | return _this; 298 | } 299 | 300 | _createClass(ReactVideoTrimmer, [{ 301 | key: "render", 302 | value: function render() { 303 | var _this$state2 = this.state, 304 | decoding = _this$state2.decoding, 305 | encoding = _this$state2.encoding, 306 | encoded = _this$state2.encoded, 307 | videoDataURL = _this$state2.videoDataURL, 308 | ffmpegReady = _this$state2.ffmpegReady; 309 | return React.createElement("div", { 310 | className: "rvt-main-container" 311 | }, !ffmpegReady && React.createElement(Status, null, React.createElement(Icon, { 312 | name: "spin", 313 | className: "rvt-icon-spin" 314 | }), this.props.loadingFFMPEGText || "PLEASE WAIT..."), ffmpegReady && encoded && React.createElement(this.VideoPlayerNoTrimmer, null), ffmpegReady && !encoded && React.createElement(React.Fragment, null, !decoding && !encoding && !videoDataURL && React.createElement(FilePicker, { 315 | onFileSelected: this.handleFileSelected, 316 | minSize: this.props.minSize, 317 | maxSize: this.props.maxSize 318 | }), (decoding || encoding) && React.createElement(Status, null, React.createElement(Icon, { 319 | name: "spin", 320 | className: "rvt-icon-spin" 321 | }), encoding ? "ENCODING VIDEO" : "DECODING VIDEO", "..."), React.createElement(this.VideoPlayerWithTrimmer, { 322 | showTrimmer: true 323 | }))); 324 | } 325 | }]); 326 | 327 | return ReactVideoTrimmer; 328 | }(React.PureComponent); 329 | 330 | _defineProperty(ReactVideoTrimmer, "propTypes", { 331 | onVideoEncode: PropTypes.func, 332 | showEncodeBtn: PropTypes.bool, 333 | timeLimit: PropTypes.number, 334 | loadingFFMPEGText: PropTypes.string 335 | }); 336 | 337 | export var preloadWebVideo = webVideoLoader; 338 | export default ReactVideoTrimmer; -------------------------------------------------------------------------------- /dist/es/libs/WebVideo.js: -------------------------------------------------------------------------------- 1 | function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 2 | 3 | function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } 4 | 5 | function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } 6 | 7 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 8 | 9 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 10 | 11 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 12 | 13 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 14 | 15 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 16 | 17 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 18 | 19 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 20 | 21 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 22 | 23 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 24 | 25 | import { EventEmitter } from "events"; 26 | import { readDataURL, arrayBufferToBlob, readArrayBuffer } from "./utils"; 27 | import workerClient from "ffmpeg-webworker"; 28 | import { fromS } from "./formatSeconds"; 29 | 30 | var WebVideo = 31 | /*#__PURE__*/ 32 | function (_EventEmitter) { 33 | _inherits(WebVideo, _EventEmitter); 34 | 35 | function WebVideo(videoFile) { 36 | var _this; 37 | 38 | _classCallCheck(this, WebVideo); 39 | 40 | _this = _possibleConstructorReturn(this, _getPrototypeOf(WebVideo).call(this)); 41 | 42 | _defineProperty(_assertThisInitialized(_this), "handleDoneClientDone", function (result) { 43 | // console.log(result); 44 | // if (!this.optimizedVideo) { 45 | // this.optimizedVideo = true; 46 | // const converted = arrayBufferToBlob(result[0].data); 47 | // // console.log(converted); 48 | // workerClient.inputFile = converted; 49 | // setTimeout(this.optimizeVideo, 500); 50 | // } else { 51 | var converted = arrayBufferToBlob(result[0].data); 52 | 53 | _this.emit("FFMPEGDone", result); // } 54 | 55 | }); 56 | 57 | _defineProperty(_assertThisInitialized(_this), "trimVideo", function () { 58 | var start = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; 59 | var length = arguments.length > 1 ? arguments[1] : undefined; 60 | var startSeconds = fromS(start, "hh:mm:ss"); 61 | workerClient.runCommand("-ss ".concat(startSeconds, " -c copy -t ").concat(length, " sliced-output.mp4")); 62 | }); 63 | 64 | _defineProperty(_assertThisInitialized(_this), "optimizeVideo", function () { 65 | workerClient.runCommand("-strict -2 -vcodec libx264 -crf 23 output.mp4", 253554432); 66 | }); 67 | 68 | _defineProperty(_assertThisInitialized(_this), "_videoData", {}); 69 | 70 | _defineProperty(_assertThisInitialized(_this), "_videoFile", null); 71 | 72 | _defineProperty(_assertThisInitialized(_this), "optimizedVideo", false); 73 | 74 | _defineProperty(_assertThisInitialized(_this), "_videoBuffer", {}); 75 | 76 | _defineProperty(_assertThisInitialized(_this), "readAsArrayBuffer", 77 | /*#__PURE__*/ 78 | _asyncToGenerator( 79 | /*#__PURE__*/ 80 | regeneratorRuntime.mark(function _callee() { 81 | return regeneratorRuntime.wrap(function _callee$(_context) { 82 | while (1) { 83 | switch (_context.prev = _context.next) { 84 | case 0: 85 | _context.next = 2; 86 | return readArrayBuffer(_this._videoFile); 87 | 88 | case 2: 89 | _this._videoBuffer = _context.sent; 90 | return _context.abrupt("return", _this.videoBuffer); 91 | 92 | case 4: 93 | case "end": 94 | return _context.stop(); 95 | } 96 | } 97 | }, _callee); 98 | }))); 99 | 100 | _defineProperty(_assertThisInitialized(_this), "convertBufferToBlob", function (buffer) { 101 | var blob = null; 102 | buffer = buffer || _this.videoBuffer; 103 | 104 | if (buffer.byteLength) { 105 | blob = arrayBufferToBlob(buffer); 106 | } 107 | 108 | return blob; 109 | }); 110 | 111 | _defineProperty(_assertThisInitialized(_this), "readAsDataURL", 112 | /*#__PURE__*/ 113 | function () { 114 | var _ref2 = _asyncToGenerator( 115 | /*#__PURE__*/ 116 | regeneratorRuntime.mark(function _callee2(buffer, blob) { 117 | var dataURL; 118 | return regeneratorRuntime.wrap(function _callee2$(_context2) { 119 | while (1) { 120 | switch (_context2.prev = _context2.next) { 121 | case 0: 122 | buffer = buffer || _this.videoBuffer; 123 | blob = blob || _this.convertBufferToBlob(buffer); 124 | dataURL = null; 125 | 126 | if (!blob) { 127 | _context2.next = 7; 128 | break; 129 | } 130 | 131 | _context2.next = 6; 132 | return readDataURL(blob); 133 | 134 | case 6: 135 | dataURL = _context2.sent; 136 | 137 | case 7: 138 | return _context2.abrupt("return", dataURL); 139 | 140 | case 8: 141 | case "end": 142 | return _context2.stop(); 143 | } 144 | } 145 | }, _callee2); 146 | })); 147 | 148 | return function (_x, _x2) { 149 | return _ref2.apply(this, arguments); 150 | }; 151 | }()); 152 | 153 | _defineProperty(_assertThisInitialized(_this), "decode", 154 | /*#__PURE__*/ 155 | function () { 156 | var _ref3 = _asyncToGenerator( 157 | /*#__PURE__*/ 158 | regeneratorRuntime.mark(function _callee3(file) { 159 | var arrayBuffer, dataURL, videoObjectUrl, video; 160 | return regeneratorRuntime.wrap(function _callee3$(_context3) { 161 | while (1) { 162 | switch (_context3.prev = _context3.next) { 163 | case 0: 164 | _this.videoFile = file; 165 | 166 | _this.emit("processingFile"); // Read File As ArrayBuffer 167 | 168 | 169 | _context3.next = 4; 170 | return _this.readAsArrayBuffer(); 171 | 172 | case 4: 173 | arrayBuffer = _context3.sent; 174 | _context3.next = 7; 175 | return _this.readAsDataURL(arrayBuffer); 176 | 177 | case 7: 178 | dataURL = _context3.sent; 179 | videoObjectUrl = URL.createObjectURL(_this.videoFile); 180 | video = document.createElement("video"); 181 | video.src = videoObjectUrl; 182 | 183 | case 11: 184 | if (!((video.duration === Infinity || isNaN(video.duration)) && video.readyState < 2)) { 185 | _context3.next = 17; 186 | break; 187 | } 188 | 189 | _context3.next = 14; 190 | return new Promise(function (r) { 191 | return setTimeout(r, 1000); 192 | }); 193 | 194 | case 14: 195 | video.currentTime = 10000000 * Math.random(); 196 | _context3.next = 11; 197 | break; 198 | 199 | case 17: 200 | _this._videoData = video; 201 | 202 | _this.emit("processedFile"); 203 | 204 | return _context3.abrupt("return", { 205 | dataURL: dataURL, 206 | arrayBuffer: arrayBuffer, 207 | blob: _this.convertBufferToBlob() 208 | }); 209 | 210 | case 20: 211 | case "end": 212 | return _context3.stop(); 213 | } 214 | } 215 | }, _callee3); 216 | })); 217 | 218 | return function (_x3) { 219 | return _ref3.apply(this, arguments); 220 | }; 221 | }()); 222 | 223 | _defineProperty(_assertThisInitialized(_this), "generateBufferChunks", function () { 224 | var arrayBuffer = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 225 | return new Promise(function (resolve, reject) { 226 | try { 227 | var chunks = []; 228 | arrayBuffer = arrayBuffer.byteLength ? arrayBuffer : _this.videoBuffer; 229 | var typedBuffer = new Uint8Array(arrayBuffer); 230 | var microSec = 1000 * 60; 231 | var startChunk = 0; 232 | 233 | for (var i = microSec; i < typedBuffer.byteLength; i += microSec) { 234 | var _buffer = arrayBuffer.slice(startChunk, i); 235 | 236 | chunks.push(_buffer); 237 | startChunk = i; 238 | } 239 | 240 | resolve(chunks); 241 | } catch (e) { 242 | reject(e); 243 | } 244 | }); 245 | }); 246 | 247 | _defineProperty(_assertThisInitialized(_this), "extractFramesFromVideo", function () { 248 | var fps = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 25; 249 | return new Promise( 250 | /*#__PURE__*/ 251 | function () { 252 | var _ref4 = _asyncToGenerator( 253 | /*#__PURE__*/ 254 | regeneratorRuntime.mark(function _callee5(resolve, reject) { 255 | var video, seekResolve, duration, canvas, context, _ref6, w, h, frames, interval, currentTime, base64ImageData; 256 | 257 | return regeneratorRuntime.wrap(function _callee5$(_context5) { 258 | while (1) { 259 | switch (_context5.prev = _context5.next) { 260 | case 0: 261 | _context5.prev = 0; 262 | 263 | _this.emit("extractingFrames"); 264 | 265 | video = _this._videoData; 266 | video.addEventListener("seeked", 267 | /*#__PURE__*/ 268 | _asyncToGenerator( 269 | /*#__PURE__*/ 270 | regeneratorRuntime.mark(function _callee4() { 271 | return regeneratorRuntime.wrap(function _callee4$(_context4) { 272 | while (1) { 273 | switch (_context4.prev = _context4.next) { 274 | case 0: 275 | if (seekResolve) seekResolve(); 276 | 277 | case 1: 278 | case "end": 279 | return _context4.stop(); 280 | } 281 | } 282 | }, _callee4); 283 | }))); 284 | duration = video.duration; 285 | canvas = document.createElement("canvas"); 286 | context = canvas.getContext("2d"); 287 | _ref6 = [video.videoWidth, video.videoHeight], w = _ref6[0], h = _ref6[1]; 288 | canvas.width = w; 289 | canvas.height = h; 290 | frames = []; 291 | interval = 125 / fps; 292 | currentTime = 0; 293 | 294 | case 13: 295 | if (!(currentTime < duration)) { 296 | _context5.next = 23; 297 | break; 298 | } 299 | 300 | video.currentTime = currentTime; 301 | _context5.next = 17; 302 | return new Promise(function (r) { 303 | return seekResolve = r; 304 | }); 305 | 306 | case 17: 307 | context.drawImage(video, 0, 0, w, h); 308 | base64ImageData = canvas.toDataURL(); 309 | frames.push(base64ImageData); 310 | currentTime += interval; 311 | _context5.next = 13; 312 | break; 313 | 314 | case 23: 315 | _this.emit("extractedFrames"); 316 | 317 | resolve(frames); 318 | _context5.next = 30; 319 | break; 320 | 321 | case 27: 322 | _context5.prev = 27; 323 | _context5.t0 = _context5["catch"](0); 324 | reject(_context5.t0); 325 | 326 | case 30: 327 | case "end": 328 | return _context5.stop(); 329 | } 330 | } 331 | }, _callee5, null, [[0, 27]]); 332 | })); 333 | 334 | return function (_x4, _x5) { 335 | return _ref4.apply(this, arguments); 336 | }; 337 | }()); 338 | }); 339 | 340 | _this.videoFile = videoFile; 341 | _this.workerClient = workerClient; 342 | workerClient.on("onReady", function () { 343 | return _this.emit("FFMPEGReady"); 344 | }); 345 | workerClient.on("onStdout", function (msg) { 346 | return _this.emit("FFMPEGStdout", msg); 347 | }); 348 | workerClient.on("onFileReceived", function () { 349 | return _this.emit("FFMPEGFileReceived"); 350 | }); 351 | workerClient.on("onDone", _this.handleDoneClientDone); 352 | return _this; 353 | } 354 | 355 | _createClass(WebVideo, [{ 356 | key: "videoFile", 357 | set: function set(file) { 358 | if (file && file.type) { 359 | workerClient.inputFile = file; 360 | } 361 | 362 | this._videoFile = file; 363 | }, 364 | get: function get() { 365 | return this._videoFile; 366 | } 367 | }, { 368 | key: "duration", 369 | get: function get() { 370 | return this._videoData.duration || 0; 371 | } 372 | }, { 373 | key: "videoData", 374 | get: function get() { 375 | return this._videoData; 376 | } 377 | }, { 378 | key: "videoBuffer", 379 | get: function get() { 380 | return this._videoBuffer; 381 | } 382 | }]); 383 | 384 | return WebVideo; 385 | }(EventEmitter); 386 | 387 | export default WebVideo; -------------------------------------------------------------------------------- /dist/es/libs/formatSeconds.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import zeroFill from "zero-fill"; 4 | /** 5 | * Original from https://www.npmjs.com/package/hh-mm-ss 6 | */ 7 | // Time units with their corresponding values in miliseconds 8 | 9 | var HOUR = 3600000; 10 | var MINUTE = 60000; 11 | var SECOND = 1000; 12 | var TIME_FORMAT_ERRMSG = "Time format error"; // ============================================================================= 13 | // Export functions 14 | // ============================================================================= 15 | 16 | export function fromMs(ms) { 17 | var format = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "mm:ss"; 18 | 19 | if (typeof ms !== "number" || Number.isNaN(ms)) { 20 | throw new Error("NaN error"); 21 | } 22 | 23 | var absMs = Math.abs(ms); 24 | var negative = ms < 0; 25 | var hours = Math.floor(absMs / HOUR); 26 | var minutes = Math.floor(absMs % HOUR / MINUTE); 27 | var seconds = Math.floor(absMs % MINUTE / SECOND); 28 | var miliseconds = Math.floor(absMs % SECOND); 29 | return formatTime({ 30 | negative: negative, 31 | hours: hours, 32 | minutes: minutes, 33 | seconds: seconds, 34 | miliseconds: miliseconds 35 | }, format); 36 | } 37 | export function fromS(s) { 38 | var format = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "mm:ss"; 39 | 40 | if (typeof s !== "number" || Number.isNaN(s)) { 41 | throw new Error("NaN error"); 42 | } 43 | 44 | var ms = s * SECOND; 45 | return fromMs(ms, format); 46 | } 47 | export function toMs(time) { 48 | var format = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "mm:ss"; 49 | var re; 50 | 51 | if (["mm:ss", "mm:ss.sss", "hh:mm:ss", "hh:mm:ss.sss"].includes(format)) { 52 | re = /^(-)?(?:(\d\d+):)?(\d\d):(\d\d)(\.\d+)?$/; 53 | } else if (format === "hh:mm") { 54 | re = /^(-)?(\d\d):(\d\d)(?::(\d\d)(?:(\.\d+))?)?$/; 55 | } else { 56 | throw new Error(TIME_FORMAT_ERRMSG); 57 | } 58 | 59 | var result = re.exec(time); 60 | if (!result) throw new Error(); 61 | var negative = result[1] === "-"; 62 | var hours = result[2] | 0; 63 | var minutes = result[3] | 0; 64 | var seconds = result[4] | 0; 65 | var miliseconds = Math.floor(1000 * result[5] | 0); 66 | 67 | if (minutes > 60 || seconds > 60) { 68 | throw new Error(); 69 | } 70 | 71 | return (negative ? -1 : 1) * (hours * HOUR + minutes * MINUTE + seconds * SECOND + miliseconds); 72 | } 73 | export function toS(time) { 74 | var format = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "mm:ss"; 75 | var ms = toMs(time, format); 76 | return Math.floor(ms / SECOND); 77 | } // ============================================================================= 78 | // Utility functions 79 | // ============================================================================= 80 | 81 | function formatTime(time, format) { 82 | var showMs; 83 | var showSc; 84 | var showHr; 85 | 86 | switch (format.toLowerCase()) { 87 | case "hh:mm:ss.sss": 88 | showMs = true; 89 | showSc = true; 90 | showHr = true; 91 | break; 92 | 93 | case "hh:mm:ss": 94 | showMs = !!time.miliseconds; 95 | showSc = true; 96 | showHr = true; 97 | break; 98 | 99 | case "hh:mm": 100 | showMs = !!time.miliseconds; 101 | showSc = showMs || !!time.seconds; 102 | showHr = true; 103 | break; 104 | 105 | case "mm:ss": 106 | showMs = !!time.miliseconds; 107 | showSc = true; 108 | showHr = !!time.hours; 109 | break; 110 | 111 | case "mm:ss.sss": 112 | showMs = true; 113 | showSc = true; 114 | showHr = !!time.hours; 115 | break; 116 | 117 | default: 118 | throw new Error(TIME_FORMAT_ERRMSG); 119 | } 120 | 121 | var hh = zeroFill(2, time.hours); 122 | var mm = zeroFill(2, time.minutes); 123 | var ss = zeroFill(2, time.seconds); 124 | var sss = zeroFill(3, time.miliseconds); 125 | return (time.negative ? "-" : "") + (showHr ? showMs ? "".concat(hh, ":").concat(mm, ":").concat(ss, ".").concat(sss) : showSc ? "".concat(hh, ":").concat(mm, ":").concat(ss) : "".concat(hh, ":").concat(mm) : showMs ? "".concat(mm, ":").concat(ss, ".").concat(sss) : "".concat(mm, ":").concat(ss)); 126 | } -------------------------------------------------------------------------------- /dist/es/libs/preloadWebVideo.js: -------------------------------------------------------------------------------- 1 | import WebVideo from "./WebVideo"; 2 | export default (function () { 3 | var opts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 4 | return new WebVideo(opts); 5 | }); -------------------------------------------------------------------------------- /dist/es/libs/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * detect if a file is an video. 3 | * @param {File} file 4 | */ 5 | export var isVideo = function isVideo(file) { 6 | return file.type.indexOf("video") > -1; 7 | }; 8 | /** 9 | * create range [min .. max] 10 | */ 11 | 12 | export var range = function range(min, max) { 13 | return Array.apply(null, { 14 | length: max - min + 1 15 | }).map(function (v, i) { 16 | return i + min; 17 | }); 18 | }; 19 | /** 20 | * FileReader via promise 21 | * @param {File} file 22 | * @param {string} dataType 23 | * @return {Promise} 24 | */ 25 | 26 | export var readFile = function readFile(file) { 27 | var dataType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "ArrayBuffer"; 28 | return new Promise(function (resolve, reject) { 29 | var reader = new FileReader(); 30 | reader["readAs" + dataType](file); 31 | 32 | reader.onload = function () { 33 | return resolve(reader.result); 34 | }; 35 | 36 | reader.onerror = function (err) { 37 | return reject(err); 38 | }; 39 | }); 40 | }; 41 | /** 42 | * Read File/Blob to ArrayBuffer 43 | * @param {File} file 44 | * @return {Promise} 45 | */ 46 | 47 | export var readArrayBuffer = function readArrayBuffer(file) { 48 | return readFile(file, "ArrayBuffer"); 49 | }; 50 | /** 51 | * Read File/Blob to Base64 52 | * @param {File} file 53 | * @return {Promise} 54 | */ 55 | 56 | export var readDataURL = function readDataURL(file) { 57 | return readFile(file, "DataURL"); 58 | }; 59 | export var readBlobURL = function readBlobURL(file) { 60 | return URL.createObjectURL(file); 61 | }; 62 | export var download = function download(url, name) { 63 | var link = document.createElement("a"); 64 | link.href = url; 65 | link.download = name; 66 | link.click(); 67 | }; 68 | export var rename = function rename(filename, ext, stamp) { 69 | return "".concat(filename.replace(/\.\w+$/, "")).concat(stamp || "", ".").concat(ext); 70 | }; 71 | /** 72 | * format seconds to [minutes, integer, decimal(2)] 73 | * @param {number} seconds 74 | */ 75 | 76 | export var formatSeconds = function formatSeconds(seconds) { 77 | return [Math.floor(seconds / 60), Math.floor(seconds % 60), Math.round(seconds % 1 * 100)]; 78 | }; 79 | export var leftZero = function leftZero(num, count) { 80 | return ("000000" + num).slice(-count); 81 | }; 82 | export var noop = function noop() {}; 83 | export var arrayBufferToBlob = function arrayBufferToBlob(buffer) { 84 | return new Blob([new Uint8Array(buffer, 0, buffer.byteLength)], { 85 | type: "video/webm", 86 | name: "video.webm" 87 | }); 88 | }; -------------------------------------------------------------------------------- /dist/es/style.js: -------------------------------------------------------------------------------- 1 | import "./styles/main-container.scss"; 2 | import "./styles/controls.scss"; 3 | import "./styles/dragger.scss"; 4 | import "./styles/file-picker.scss"; 5 | import "./styles/icon.scss"; 6 | import "./styles/player.scss"; 7 | import "./styles/status.scss"; 8 | import "./styles/trimmer.scss"; -------------------------------------------------------------------------------- /dist/es/styles/controls.scss: -------------------------------------------------------------------------------- 1 | .btn-reset { 2 | padding: 0; 3 | border: 0; 4 | outline: 0; 5 | background: none; 6 | } 7 | 8 | .rvt-controls-cont { 9 | margin-top: 10px; 10 | text-align: center; 11 | .seconds { 12 | font-size: 12px; 13 | line-height: 36px; 14 | margin-left: 10px; 15 | display: inline-block; 16 | overflow: hidden; 17 | color: #aaa; 18 | } 19 | } 20 | 21 | @keyframes spin { 22 | from { 23 | transform: rotate(0); 24 | } 25 | 26 | to { 27 | transform: rotate(360deg); 28 | } 29 | } 30 | 31 | .rvt-icon-spin { 32 | animation: spin infinite 1s linear; 33 | } 34 | 35 | .rvt-controller-item, 36 | .rvt-file-picker-control { 37 | @extend .btn-reset; 38 | 39 | display: inline-block; 40 | font-size: 16px; 41 | text-align: center; 42 | color: #999; 43 | padding: 10px; 44 | margin-right: 10px; 45 | box-shadow: 0 0 15px rgba(0, 0, 0, 0.1); 46 | cursor: pointer; 47 | 48 | &:hover { 49 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.16); 50 | } 51 | 52 | .rvt-icon { 53 | display: block; 54 | } 55 | } 56 | 57 | .rvt-controller-dropdown { 58 | display: inline-block; 59 | position: relative; 60 | 61 | .rvt-controller-list-wrap { 62 | position: relative; 63 | } 64 | 65 | .rvt-controller-list { 66 | position: absolute; 67 | width: 60px; 68 | top: 0; 69 | left: 0; 70 | visibility: hidden; 71 | opacity: 0; 72 | transition-durvtion: 0.3s; 73 | transition-property: opacity, visibility; 74 | list-style: none; 75 | background: #fff; 76 | box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); 77 | padding: 0; 78 | margin: 0; 79 | z-index: 10; 80 | 81 | a { 82 | @extend .btn-reset; 83 | 84 | display: block; 85 | width: 100%; 86 | text-align: center; 87 | padding: 5px 0; 88 | color: inherit; 89 | font-size: 12px; 90 | cursor: pointer; 91 | 92 | &:hover { 93 | background: #333; 94 | color: #fff; 95 | } 96 | } 97 | } 98 | 99 | &:hover { 100 | .rvt-controller-list { 101 | opacity: 1; 102 | visibility: visible; 103 | } 104 | } 105 | } 106 | 107 | .rvt-controller-clipper { 108 | position: absolute; 109 | width: 100%; 110 | height: 100%; 111 | } 112 | -------------------------------------------------------------------------------- /dist/es/styles/dragger.scss: -------------------------------------------------------------------------------- 1 | .rvt-dragger { 2 | position: absolute; 3 | top: 0; 4 | bottom: 0; 5 | width: 1px; 6 | background: #999; 7 | cursor: col-resize; 8 | 9 | &::after { 10 | content: ""; 11 | position: absolute; 12 | left: -2px; 13 | right: -2px; 14 | top: 0; 15 | bottom: 0; 16 | } 17 | 18 | &:hover { 19 | background: #333; 20 | } 21 | } 22 | 23 | .rvt-drag-current { 24 | background: #038c7f; 25 | } 26 | -------------------------------------------------------------------------------- /dist/es/styles/file-picker.scss: -------------------------------------------------------------------------------- 1 | .rvt-file-picker { 2 | width: 100%; 3 | text-align: center; 4 | padding: 50px; 5 | border: 1px solid #038c7f; 6 | background-color: #ccfdf856; 7 | cursor: pointer; 8 | color: #038c7f; 9 | & input { 10 | visibility: hidden; 11 | position: absolute; 12 | right: 0; 13 | left: 0; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /dist/es/styles/icon.scss: -------------------------------------------------------------------------------- 1 | .rvt-icon { 2 | width: 1em; 3 | height: 1em; 4 | fill: #038c7f; 5 | vertical-align: middle; 6 | } 7 | -------------------------------------------------------------------------------- /dist/es/styles/main-container.scss: -------------------------------------------------------------------------------- 1 | .btn-reset { 2 | padding: 0; 3 | border: 0; 4 | outline: 0; 5 | background: none; 6 | } 7 | 8 | .rvt-main-container { 9 | > * { 10 | box-sizing: border-box; 11 | } 12 | .rvt-icon-spin { 13 | animation: spin infinite 1s linear; 14 | } 15 | } 16 | @keyframes spin { 17 | from { 18 | transform: rotate(0); 19 | } 20 | 21 | to { 22 | transform: rotate(360deg); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /dist/es/styles/player.scss: -------------------------------------------------------------------------------- 1 | .rvt-player-cont { 2 | position: relative; 3 | // &::before { 4 | // -webkit-user-drag: none; 5 | // -khtml-user-drag: none; 6 | // -moz-user-drag: none; 7 | // -o-user-drag: none; 8 | // user-drag: none; 9 | 10 | // -webkit-touch-callout: none; /* iOS Safari */ 11 | // -webkit-user-select: none; /* Chrome/Safari/Opera */ 12 | // -khtml-user-select: none; /* Konqueror */ 13 | // -moz-user-select: none; /* Firefox */ 14 | // -ms-user-select: none; /* Internet Explorer/Edge*/ 15 | // user-select: none; /* Non-prefixed version, currently 16 | // not supported by any browser */ 17 | 18 | // content: ""; 19 | // width: 100%; 20 | // height: 100%; 21 | // position: absolute; 22 | // left: 0; 23 | // top: 0; 24 | // } 25 | & .rvt-player-time-range-cont { 26 | margin: 5px; 27 | text-align: center; 28 | & .rvt-player-time-range { 29 | color: rgb(143, 143, 143); 30 | font-size: 14px; 31 | padding: 5px; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /dist/es/styles/status.scss: -------------------------------------------------------------------------------- 1 | .rvt-status { 2 | width: 100%; 3 | text-align: center; 4 | padding: 50px; 5 | background-color: #ccfdf856; 6 | cursor: pointer; 7 | font-size: 20px; 8 | color: #038c7f; 9 | & input { 10 | visibility: hidden; 11 | position: absolute; 12 | right: 0; 13 | left: 0; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /dist/es/styles/trimmer.scss: -------------------------------------------------------------------------------- 1 | .rvt-trimmer-cont { 2 | display: block; 3 | width: 100%; 4 | height: 15px; 5 | position: relative; 6 | margin-bottom: 20px; 7 | margin-top: 30px; 8 | background-color: #ccfdf856; 9 | 10 | > * { 11 | box-sizing: border-box; 12 | } 13 | & .rvt-thumb { 14 | position: relative; 15 | display: inline-block; 16 | } 17 | } 18 | .rvt-trimmer { 19 | background: #038c7f; 20 | position: absolute; 21 | top: 0; 22 | right: 0; 23 | width: 100%; 24 | height: 100%; 25 | z-index: 1200; 26 | } 27 | 28 | // cursorColor = #0cf; 29 | .rvt-player-cursor-current { 30 | position: absolute; 31 | font-size: 12px; 32 | top: -22px; 33 | padding: 1px 3px; 34 | text-align: center; 35 | color: #fff; 36 | transform: translate(-50%) scale(0.8); 37 | background: #038c7f; 38 | 39 | .rvt-player-num { 40 | font-family: monospace; 41 | } 42 | 43 | &::after { 44 | content: ""; 45 | position: absolute; 46 | border: 5px solid transparent; 47 | border-top-color: #038c7f; 48 | bottom: -9px; 49 | left: 50%; 50 | margin-left: -5px; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /dist/style.css: -------------------------------------------------------------------------------- 1 | .btn-reset, .rvt-controller-item, 2 | .rvt-file-picker-control, .rvt-controller-dropdown .rvt-controller-list a { 3 | padding: 0; 4 | border: 0; 5 | outline: 0; 6 | background: none; } 7 | 8 | .rvt-main-container > * { 9 | box-sizing: border-box; } 10 | 11 | .rvt-main-container .rvt-icon-spin { 12 | animation: spin infinite 1s linear; } 13 | 14 | @keyframes spin { 15 | from { 16 | transform: rotate(0); } 17 | to { 18 | transform: rotate(360deg); } } 19 | 20 | .btn-reset, .rvt-controller-item, 21 | .rvt-file-picker-control, .rvt-controller-dropdown .rvt-controller-list a { 22 | padding: 0; 23 | border: 0; 24 | outline: 0; 25 | background: none; } 26 | 27 | .rvt-controls-cont { 28 | margin-top: 10px; 29 | text-align: center; } 30 | .rvt-controls-cont .seconds { 31 | font-size: 12px; 32 | line-height: 36px; 33 | margin-left: 10px; 34 | display: inline-block; 35 | overflow: hidden; 36 | color: #aaa; } 37 | 38 | @keyframes spin { 39 | from { 40 | transform: rotate(0); } 41 | to { 42 | transform: rotate(360deg); } } 43 | 44 | .rvt-icon-spin { 45 | animation: spin infinite 1s linear; } 46 | 47 | .rvt-controller-item, 48 | .rvt-file-picker-control { 49 | display: inline-block; 50 | font-size: 16px; 51 | text-align: center; 52 | color: #999; 53 | padding: 10px; 54 | margin-right: 10px; 55 | box-shadow: 0 0 15px rgba(0, 0, 0, 0.1); 56 | cursor: pointer; } 57 | .rvt-controller-item:hover, 58 | .rvt-file-picker-control:hover { 59 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.16); } 60 | .rvt-controller-item .rvt-icon, 61 | .rvt-file-picker-control .rvt-icon { 62 | display: block; } 63 | 64 | .rvt-controller-dropdown { 65 | display: inline-block; 66 | position: relative; } 67 | .rvt-controller-dropdown .rvt-controller-list-wrap { 68 | position: relative; } 69 | .rvt-controller-dropdown .rvt-controller-list { 70 | position: absolute; 71 | width: 60px; 72 | top: 0; 73 | left: 0; 74 | visibility: hidden; 75 | opacity: 0; 76 | transition-durvtion: 0.3s; 77 | transition-property: opacity, visibility; 78 | list-style: none; 79 | background: #fff; 80 | box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); 81 | padding: 0; 82 | margin: 0; 83 | z-index: 10; } 84 | .rvt-controller-dropdown .rvt-controller-list a { 85 | display: block; 86 | width: 100%; 87 | text-align: center; 88 | padding: 5px 0; 89 | color: inherit; 90 | font-size: 12px; 91 | cursor: pointer; } 92 | .rvt-controller-dropdown .rvt-controller-list a:hover { 93 | background: #333; 94 | color: #fff; } 95 | .rvt-controller-dropdown:hover .rvt-controller-list { 96 | opacity: 1; 97 | visibility: visible; } 98 | 99 | .rvt-controller-clipper { 100 | position: absolute; 101 | width: 100%; 102 | height: 100%; } 103 | 104 | .rvt-dragger { 105 | position: absolute; 106 | top: 0; 107 | bottom: 0; 108 | width: 1px; 109 | background: #999; 110 | cursor: col-resize; } 111 | .rvt-dragger::after { 112 | content: ""; 113 | position: absolute; 114 | left: -2px; 115 | right: -2px; 116 | top: 0; 117 | bottom: 0; } 118 | .rvt-dragger:hover { 119 | background: #333; } 120 | 121 | .rvt-drag-current { 122 | background: #038c7f; } 123 | 124 | .rvt-icon { 125 | width: 1em; 126 | height: 1em; 127 | fill: #038c7f; 128 | vertical-align: middle; } 129 | 130 | .rvt-file-picker { 131 | width: 100%; 132 | text-align: center; 133 | padding: 50px; 134 | border: 1px solid #038c7f; 135 | background-color: #ccfdf856; 136 | cursor: pointer; 137 | color: #038c7f; } 138 | .rvt-file-picker input { 139 | visibility: hidden; 140 | position: absolute; 141 | right: 0; 142 | left: 0; } 143 | 144 | .rvt-player-cont { 145 | position: relative; } 146 | .rvt-player-cont .rvt-player-time-range-cont { 147 | margin: 5px; 148 | text-align: center; } 149 | .rvt-player-cont .rvt-player-time-range-cont .rvt-player-time-range { 150 | color: #8f8f8f; 151 | font-size: 14px; 152 | padding: 5px; } 153 | 154 | .rvt-status { 155 | width: 100%; 156 | text-align: center; 157 | padding: 50px; 158 | background-color: #ccfdf856; 159 | cursor: pointer; 160 | font-size: 20px; 161 | color: #038c7f; } 162 | .rvt-status input { 163 | visibility: hidden; 164 | position: absolute; 165 | right: 0; 166 | left: 0; } 167 | 168 | .rvt-trimmer-cont { 169 | display: block; 170 | width: 100%; 171 | height: 15px; 172 | position: relative; 173 | margin-bottom: 20px; 174 | margin-top: 30px; 175 | background-color: #ccfdf856; } 176 | .rvt-trimmer-cont > * { 177 | box-sizing: border-box; } 178 | .rvt-trimmer-cont .rvt-thumb { 179 | position: relative; 180 | display: inline-block; } 181 | 182 | .rvt-trimmer { 183 | background: #038c7f; 184 | position: absolute; 185 | top: 0; 186 | right: 0; 187 | width: 100%; 188 | height: 100%; 189 | z-index: 1200; } 190 | 191 | .rvt-player-cursor-current { 192 | position: absolute; 193 | font-size: 12px; 194 | top: -22px; 195 | padding: 1px 3px; 196 | text-align: center; 197 | color: #fff; 198 | transform: translate(-50%) scale(0.8); 199 | background: #038c7f; } 200 | .rvt-player-cursor-current .rvt-player-num { 201 | font-family: monospace; } 202 | .rvt-player-cursor-current::after { 203 | content: ""; 204 | position: absolute; 205 | border: 5px solid transparent; 206 | border-top-color: #038c7f; 207 | bottom: -9px; 208 | left: 50%; 209 | margin-left: -5px; } 210 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | react-video-trimmer 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/BASE.MD: -------------------------------------------------------------------------------- 1 | # react-video-trimmer 2 | 3 | Amazing React component for manipulating video length 4 | 5 | ```js 6 | import ReactVideoTrimmer, { preloadWebVideo } from "react-video-trimmer"; 7 | // Redundant, ReactVideoTrimmer is in scope, only call when it is not in scope and you require preloading of ffmpeg.js 8 | preloadWebVideo({}); 9 | 10 |
11 | 17 |
; 18 | ``` 19 | -------------------------------------------------------------------------------- /examples/theme.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | font-family: sans-serif; 5 | } 6 | 7 | .container > p { 8 | font-size: 1rem; 9 | } 10 | 11 | .container > em { 12 | font-size: 0.8rem; 13 | } 14 | 15 | .dropzone { 16 | flex: 1; 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | padding: 20px; 21 | border-width: 2px; 22 | border-radius: 2px; 23 | border-color: #eeeeee; 24 | border-style: dashed; 25 | background-color: #fafafa; 26 | color: #bdbdbd; 27 | outline: none; 28 | transition: border 0.24s ease-in-out; 29 | } 30 | 31 | .dropzone:focus { 32 | border-color: #2196f3; 33 | } 34 | 35 | .dropzone.disabled { 36 | opacity: 0.6; 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-video-trimmer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist/" 8 | ], 9 | "scripts": { 10 | "clean": "rimraf ./dist", 11 | "test": "cross-env NODE_ENV=test npm run eslint:src && jest --coverage && npm run typescript", 12 | "build": "npm run clean && npm run build:umd && npm run build:es", 13 | "build:umd": "cross-env NODE_ENV=es rollup -c", 14 | "build:es": "cross-env BABEL_ENV=es babel ./src --out-dir ./dist/es --ignore '**/*.spec.js'", 15 | "start": "styleguidist server", 16 | "styleguide": "styleguidist build", 17 | "test:watch": "cross-env NODE_ENV=test jest --watch", 18 | "eslint:src": "eslint .", 19 | "commitmsg": "commitlint -e", 20 | "precommit": "pretty-quick --staged", 21 | "prepublish": "npm run build", 22 | "logo": "cd logo && sketchtool export artboards logo.sketch", 23 | "imagemin": "imagemin --out-dir=logo --plugin=pngquant --plugin=svgo", 24 | "size": "size-limit", 25 | "size:why": "size-limit --why", 26 | "typescript": "tsc --project ./typings/tests" 27 | }, 28 | "size-limit": [ 29 | { 30 | "path": "dist/index.js", 31 | "limit": "7 KB" 32 | }, 33 | { 34 | "path": "dist/es/index.js", 35 | "limit": "7 KB" 36 | } 37 | ], 38 | "keywords": [ 39 | "react-component", 40 | "video", 41 | "trim", 42 | "ffmpeg", 43 | "react" 44 | ], 45 | "author": "", 46 | "license": "ISC", 47 | "peerDependencies": { 48 | "react": ">= 16.8" 49 | }, 50 | "dependencies": { 51 | "attr-accept": "^1.1.3", 52 | "eslint-config-airbnb": "^17.1.1", 53 | "ffmpeg-webworker": "^1.3.0", 54 | "ffmpeg.js": "^3.1.9001", 55 | "file-selector": "^0.1.11", 56 | "hh-mm-ss": "^1.2.0", 57 | "prop-types": "^15.7.2", 58 | "react-dropzone": "^10.1.9", 59 | "react-player": "^1.11.1", 60 | "webpack-blocks": "^2.0.1", 61 | "webworker-file": "^0.2.0", 62 | "zero-fill": "^2.2.3" 63 | }, 64 | "devDependencies": { 65 | "@babel/cli": "^7.5.5", 66 | "@babel/core": "^7.5.5", 67 | "@babel/plugin-proposal-class-properties": "^7.5.5", 68 | "@babel/plugin-proposal-do-expressions": "^7.5.0", 69 | "@babel/plugin-proposal-export-default-from": "^7.5.2", 70 | "@babel/plugin-proposal-logical-assignment-operators": "^7.2.0", 71 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.4.4", 72 | "@babel/plugin-proposal-optional-chaining": "^7.2.0", 73 | "@babel/plugin-proposal-pipeline-operator": "^7.5.0", 74 | "@babel/plugin-transform-runtime": "^7.5.5", 75 | "@babel/preset-env": "^7.5.5", 76 | "@babel/preset-react": "^7.0.0", 77 | "@babel/register": "^7.0.0", 78 | "@commitlint/cli": "^7.0.0", 79 | "@commitlint/config-angular": "^7.0.1", 80 | "@commitlint/prompt": "^7.0.0", 81 | "@commitlint/prompt-cli": "^7.0.0", 82 | "@types/react": "^16.8.3", 83 | "@types/react-dom": "^16.8.1", 84 | "babel-plugin-add-module-exports": "^1.0.0", 85 | "babel-plugin-dynamic-import-node": "^2.2.0", 86 | "babel-polyfill": "^6.26.0", 87 | "commitizen": "^2.10.1", 88 | "cross-env": "^5.2.0", 89 | "husky": "^0.14.3", 90 | "imagemin-cli": "^3.0.0", 91 | "imagemin-pngquant": "^6.0.0", 92 | "lint-staged": "^7.2.2", 93 | "markdownlint-cli": "^0.13.0", 94 | "prettier": "*", 95 | "pretty-quick": "^1.11.1", 96 | "react": "^16.8.2", 97 | "react-dom": "^16.8.2", 98 | "react-styleguidist": "^9.0.1", 99 | "rimraf": "^2.5.2", 100 | "rollup": "^1.17.0", 101 | "rollup-plugin-babel": "^4.3.3", 102 | "rollup-plugin-commonjs": "^10.0.1", 103 | "rollup-plugin-copy": "^3.0.0", 104 | "rollup-plugin-node-builtins": "^2.1.2", 105 | "rollup-plugin-node-globals": "^1.4.0", 106 | "rollup-plugin-node-resolve": "^5.2.0", 107 | "rollup-plugin-scss": "^1.0.1", 108 | "rollup-plugin-uglify": "^6.0.2", 109 | "sinon": "^3.2.1", 110 | "size-limit": "^0.19.2", 111 | "style-loader": "^0.18.2", 112 | "styled-components": "^4.1.2", 113 | "tslint": "^5.9.1", 114 | "typescript": "^3.2.4", 115 | "webpack": "^4.29.5" 116 | }, 117 | "repository": { 118 | "type": "git", 119 | "url": "https://github.com/limistah/react-video-trimmer.git" 120 | }, 121 | "config": { 122 | "commitizen": { 123 | "path": "@commitlint/prompt" 124 | } 125 | }, 126 | "husky": { 127 | "hooks": { 128 | "prepare-commit-msg": "exec < /dev/tty && git cz --hook" 129 | } 130 | }, 131 | "engines": { 132 | "node": ">= 8" 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from "rollup-plugin-babel"; 2 | import resolve from "rollup-plugin-node-resolve"; 3 | import commonjs from "rollup-plugin-commonjs"; 4 | import { uglify } from "rollup-plugin-uglify"; 5 | import scss from "rollup-plugin-scss"; 6 | import builtins from "rollup-plugin-node-builtins"; 7 | import globals from "rollup-plugin-node-globals"; 8 | import copy from "rollup-plugin-copy"; 9 | 10 | const umdGlobals = { 11 | react: "React", 12 | "prop-types": "PropTypes", 13 | Blob: "Blob", 14 | URL: "URL", 15 | Worker: "Worker", 16 | FileReader: "FileReader", 17 | Uint8Array: "Uint8Array" 18 | }; 19 | 20 | export default { 21 | input: "./src/index.js", 22 | output: { 23 | file: "dist/index.js", 24 | format: "umd", 25 | name: "reactVideoTrimmer", 26 | globals: umdGlobals, 27 | sourcemap: "inline", 28 | exports: "named" 29 | }, 30 | external: Object.keys(umdGlobals), 31 | plugins: [ 32 | babel({ 33 | exclude: "node_modules/**" 34 | }), 35 | resolve({ preferBuiltins: true }), 36 | globals(), 37 | builtins(), 38 | commonjs(), 39 | scss({ 40 | output: "./dist/style.css", 41 | failOnError: true 42 | }), 43 | copy({ 44 | targets: [{ src: "src/styles/*", dest: "dist/es/styles" }] 45 | }), 46 | uglify() 47 | ] 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/Controls.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Icon from "./Icon"; 3 | 4 | const Controls = ({ 5 | onPlayPauseClick, 6 | playing, 7 | onReselectFile, 8 | processing, 9 | onEncode, 10 | showEncodeBtn, 11 | canDownload, 12 | onDownload 13 | }) => { 14 | return ( 15 |
16 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | {showEncodeBtn && ( 32 |
33 | {canDownload ? ( 34 | 35 | 36 | 37 | ) : ( 38 | 39 | 40 | 41 | )} 42 |
43 | )} 44 |
45 | ); 46 | }; 47 | 48 | export default Controls; 49 | -------------------------------------------------------------------------------- /src/components/Dragger.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | 3 | import PropTypes from "prop-types"; 4 | 5 | export default class Dragger extends PureComponent { 6 | _screenX = null; 7 | _screenY = null; 8 | _ox = null; 9 | _oy = null; 10 | 11 | handleMouseDown = e => { 12 | this._screenX = e.screenX; 13 | this._screenY = e.screenY; 14 | this._ox = this.props.x; 15 | this._oy = this.props.y; 16 | 17 | window.addEventListener("mousemove", this.handleMouseMove, false); 18 | window.addEventListener("mouseup", this.handleMouseUp, false); 19 | }; 20 | 21 | handleMouseMove = e => { 22 | this.props.onDrag({ 23 | x: e.screenX - this._screenX + this._ox, 24 | y: e.screenY - this._screenY + this._oy 25 | }); 26 | }; 27 | 28 | handleMouseUp = () => { 29 | window.removeEventListener("mousemove", this.handleMouseMove); 30 | window.removeEventListener("mouseup", this.handleMouseUp); 31 | const handler = this.props.onDragStop || (() => {}); 32 | handler(); 33 | }; 34 | 35 | render() { 36 | return ( 37 |
45 | {this.props.children} 46 |
47 | ); 48 | } 49 | 50 | static defaultProps = { 51 | onDrag() {}, 52 | x: 0, 53 | y: 0 54 | }; 55 | 56 | static propTypes = { 57 | x: PropTypes.number, 58 | y: PropTypes.number, 59 | onDrag: PropTypes.func, 60 | className: PropTypes.string, 61 | children: PropTypes.element 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/components/FilePicker.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useRef } from "react"; 2 | import { isVideo, noop } from "../libs/utils"; 3 | import Icon from "./Icon"; 4 | import Dropzone from "react-dropzone"; 5 | 6 | function FilePicker(props) { 7 | const onDrop = useCallback(acceptedFiles => { 8 | if (acceptedFiles.length) { 9 | const video = acceptedFiles[0]; 10 | const handler = props.onFileSelected || noop; 11 | handler(video); 12 | } 13 | }, []); 14 | const MAX_SIZE = props.maxSize || 10000024; 15 | const MIN_SIZE = props.minSize || 0; 16 | // const handleFileChange = useCallback(e => { 17 | // if (e.target.files.length) { 18 | // const video = e.target.files[0]; 19 | // if (isVideo(video)) { 20 | // const handler = props.onFileSelected || noop; 21 | // handler(video); 22 | // } else { 23 | // return alert("Unsupported File Type"); 24 | // } 25 | // } 26 | // }); 27 | const toMB = byte => Math.round(byte / 1000000); 28 | return ( 29 | 35 | {({ getRootProps, getInputProps, isDragActive }) => ( 36 |
37 | 38 | 39 | {isDragActive ? ( 40 |

Drop the video here ...

41 | ) : ( 42 | <> 43 |

Drag 'n' drop a video here, or click to select one

44 |

45 | 46 | ({toMB(MIN_SIZE)} - {toMB(MAX_SIZE)}MB) 47 | 48 |

49 | 50 | )} 51 |
52 | )} 53 |
54 | ); 55 | } 56 | 57 | export default FilePicker; 58 | -------------------------------------------------------------------------------- /src/components/Icon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const Download = ({ className }) => ( 5 | 10 | 11 | 12 | 13 | ); 14 | 15 | const Music = ({ className }) => ( 16 | 21 | 22 | 23 | 24 | ); 25 | 26 | const Play = ({ className }) => ( 27 | 34 | 35 | 36 | 37 | ); 38 | 39 | const Pause = ({ className }) => ( 40 | 47 | 48 | 49 | 50 | ); 51 | 52 | const Replay = ({ className }) => ( 53 | 58 | 59 | 60 | 61 | ); 62 | 63 | const Spin = ({ className }) => ( 64 | 69 | 70 | 71 | ); 72 | 73 | const Icon = props => { 74 | let El = Download; 75 | switch (props.name) { 76 | case "music": 77 | El = Music; 78 | break; 79 | case "play": 80 | El = Play; 81 | break; 82 | case "pause": 83 | El = Pause; 84 | break; 85 | case "replay": 86 | El = Replay; 87 | break; 88 | case "spin": 89 | El = Spin; 90 | break; 91 | default: 92 | El = Download; 93 | break; 94 | } 95 | return ( 96 | 99 | ); 100 | }; 101 | 102 | Icon.propTypes = { 103 | name: PropTypes.string 104 | }; 105 | 106 | export default Icon; 107 | -------------------------------------------------------------------------------- /src/components/Player.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactPlayer from "react-player"; 3 | import { formatSeconds, noop, leftZero } from "../libs/utils"; 4 | 5 | class Player extends React.Component { 6 | state = { 7 | playing: this.props.playVideo || false 8 | }; 9 | componentWillReceiveProps(newProps) { 10 | const newTimeRange = newProps.timeRange; 11 | const oldTimeRange = this.props.timeRange; 12 | const canSeek = 13 | (oldTimeRange && newTimeRange.start !== oldTimeRange.start) || 14 | (!oldTimeRange && newTimeRange.start > 0); 15 | if (canSeek) { 16 | this.setState({ playing: false }); 17 | this.player.seekTo(newTimeRange.start, "seconds"); 18 | } 19 | if (newProps.playVideo !== this.props.playVideo) { 20 | this.setState({ playing: newProps.playVideo }); 21 | } 22 | } 23 | handlePlayerProgress = data => { 24 | if (data.loaded) { 25 | const { playedSeconds } = data; 26 | const startTimeRange = this.props.timeRange.start; 27 | const endTimeRange = this.props.timeRange.end; 28 | const playedSecondsIsLowerThanStartTime = playedSeconds <= startTimeRange; 29 | const playedSecondsIsGreaterThanEndTime = playedSeconds >= endTimeRange; 30 | if (playedSecondsIsLowerThanStartTime) { 31 | this.player.seekTo(startTimeRange, "seconds"); 32 | } 33 | if (playedSecondsIsGreaterThanEndTime) { 34 | this.player.seekTo(startTimeRange, "seconds"); 35 | // this.setState({ playing: false }); 36 | } 37 | const handler = this.props.onPlayerProgress || noop; 38 | handler(playedSeconds); 39 | } 40 | }; 41 | displaySeconds(seconds) { 42 | return seconds.toFixed(2) + "s"; 43 | } 44 | handleOnPause = () => { 45 | const handler = this.props.onPlayerPause || noop; 46 | handler(); 47 | }; 48 | handleOnPlay = () => { 49 | const handler = this.props.onPlayerPlay || noop; 50 | handler(); 51 | }; 52 | render() { 53 | const { start, end } = this.props.timeRange; 54 | return ( 55 |
{}}> 56 | {/*
81 | ); 82 | } 83 | } 84 | 85 | export default Player; 86 | -------------------------------------------------------------------------------- /src/components/Status.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Status = ({ children }) => { 4 | return
{children}
; 5 | }; 6 | 7 | export default Status; 8 | -------------------------------------------------------------------------------- /src/components/Trimmer.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import Dragger from "./Dragger"; 3 | import { noop, formatSeconds, leftZero } from "../libs/utils"; 4 | 5 | const TrimmerOverLay = props => { 6 | return ( 7 |
11 | ); 12 | }; 13 | 14 | const TimeStamp = props => { 15 | const formated = formatSeconds(props.time); 16 | return ( 17 |
18 | {formated[0]}' 19 | {formated[1]} 20 | {!props.noMicroSeconds && ( 21 | <> 22 | .{leftZero(formated[2], 2)} 23 | 24 | )} 25 |
26 | ); 27 | }; 28 | 29 | class Trimmer extends PureComponent { 30 | pos2Time = pos => { 31 | return pos / this.props.widthDurationRatio; 32 | }; 33 | 34 | time2pos = time => { 35 | return time * this.props.widthDurationRatio; 36 | }; 37 | 38 | keepInRange = x => { 39 | const containerWidth = this.props.containerWidth; 40 | if (x < 0) { 41 | return 0; 42 | } 43 | 44 | if (x > containerWidth) { 45 | return containerWidth; 46 | } 47 | 48 | return x; 49 | }; 50 | 51 | withinTimeLimit = (time, isDragEnd = true) => { 52 | const timeLimit = this.props.timeLimit; 53 | 54 | let startTime = this.props.startTime; 55 | let endTime = time; 56 | 57 | if (!isDragEnd) { 58 | startTime = time; 59 | endTime = this.props.endTime; 60 | } 61 | 62 | const duration = this.props.duration; 63 | let timeTillEnd = duration - endTime; 64 | 65 | const currentRange = duration - startTime - timeTillEnd; 66 | return timeLimit ? currentRange <= timeLimit : true; 67 | }; 68 | 69 | withinTimeRange = (time, isDragEnd = true) => { 70 | const timeRange = this.props.timeRangeLimit; 71 | let interval = time - this.props.startTime; 72 | if (!isDragEnd) { 73 | interval = this.props.endTime - time; 74 | } 75 | return timeRange ? interval >= timeRange : true; 76 | }; 77 | 78 | handleDragStart = pos => { 79 | const pos2Time = this.pos2Time(this.keepInRange(pos.x)); 80 | let time = pos2Time; 81 | 82 | const currentTime = this.props.currentTime; 83 | const currentTimeIsWithinRange = this.withinTimeRange(time, false); 84 | const currentTimeIsWithinLimit = this.withinTimeLimit(time, false); 85 | 86 | if ( 87 | time >= currentTime || 88 | !currentTimeIsWithinRange || 89 | !currentTimeIsWithinLimit 90 | ) { 91 | time = this.props.startTime; 92 | const handler = this.props.onPausePlayer || (() => {}); 93 | handler(); 94 | } 95 | this.props.onStartTimeChange(time); 96 | }; 97 | handleDragEnd = pos => { 98 | const pos2Time = this.pos2Time(this.keepInRange(pos.x)); 99 | let time = pos2Time; 100 | 101 | const endTime = this.props.endTime; 102 | const currentTime = this.props.currentTime; 103 | 104 | const currentTimeIsWithinRange = this.withinTimeRange(time); 105 | const currentTimeIsWithinLimit = this.withinTimeLimit(time); 106 | 107 | if ( 108 | currentTime >= time || 109 | !currentTimeIsWithinRange || 110 | !currentTimeIsWithinLimit 111 | ) { 112 | time = this.props.endTime; 113 | const handler = this.props.onPausePlayer || (() => {}); 114 | handler(); 115 | } 116 | 117 | this.props.onEndTimeChange(time); 118 | }; 119 | handleDragStop = () => { 120 | const handler = this.props.onGetData || noop; 121 | handler({ start: this.props.startTime, end: this.props.endTime }); 122 | }; 123 | getTrimmerWidth = width => { 124 | return this.props.containerWidth - width; 125 | }; 126 | render() { 127 | const start = this.time2pos(this.props.startTime); 128 | const end = this.time2pos(this.props.endTime); 129 | const current = this.time2pos(this.props.currentTime); 130 | return ( 131 | 132 | 133 | 138 | 139 | 140 | {}} onDragStop={() => {}}> 141 | 142 | 143 | 148 | 149 | 150 | 151 | 152 | ); 153 | } 154 | } 155 | 156 | export class VideoTrimmer extends PureComponent { 157 | state = { 158 | start: 0, 159 | end: 0 160 | }; 161 | get widthDurationRatio() { 162 | return this.containerWidth / this.props.duration; 163 | } 164 | get containerWidth() { 165 | return this.containerRef.getBoundingClientRect().width; 166 | } 167 | 168 | handleStartTimeChange = time => { 169 | this.setState({ start: time }); 170 | }; 171 | handleGetTrimData = () => { 172 | const trimmerHandler = this.props.onTrim || noop; 173 | setTimeout( 174 | () => 175 | trimmerHandler({ 176 | start: this.state.start || this.props.timeRange.start, 177 | end: this.state.end || this.props.timeRange.end 178 | }), 179 | 200 180 | ); 181 | }; 182 | handleEndTimeChange = time => { 183 | this.setState({ end: time }); 184 | }; 185 | 186 | render() { 187 | return ( 188 |
(this.containerRef = e)}> 189 | {this.props.showTrimmer && ( 190 | 203 | )} 204 |
205 | ); 206 | } 207 | } 208 | 209 | export default VideoTrimmer; 210 | -------------------------------------------------------------------------------- /src/icons/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/music.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/replay.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/spin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import FilePicker from "./components/FilePicker"; 3 | import Status from "./components/Status"; 4 | import Player from "./components/Player"; 5 | import Controls from "./components/Controls"; 6 | import Trimmer from "./components/Trimmer"; 7 | import WebVideo from "./libs/WebVideo"; 8 | import webVideoLoader from "./libs/preloadWebVideo"; 9 | import Icon from "./components/Icon"; 10 | import { noop, arrayBufferToBlob, readBlobURL, download } from "./libs/utils"; 11 | import "./style.js"; 12 | import PropTypes from "prop-types"; 13 | 14 | class ReactVideoTrimmer extends React.PureComponent { 15 | /** 16 | * @type {WebVideo} 17 | */ 18 | webVideo = webVideoLoader({}); 19 | 20 | static propTypes = { 21 | onVideoEncode: PropTypes.func, 22 | showEncodeBtn: PropTypes.bool, 23 | timeLimit: PropTypes.number, 24 | loadingFFMPEGText: PropTypes.string 25 | }; 26 | 27 | constructor(props) { 28 | super(props); 29 | this.webVideo.on("processingFile", () => this.updateIsDecoding(true)); 30 | this.webVideo.on("processedFile", () => this.updateIsDecoding(false)); 31 | 32 | this.webVideo.on("FFMPEGStdout", this.handleFFMPEGStdout); 33 | this.webVideo.on("FFMPEGReady", this.handleFFMPEGReady); 34 | this.webVideo.on("FFMPEGFileReceived", this.handleFFMPEGFileReceived); 35 | this.webVideo.on("FFMPEGDone", this.handleFFMPEGDone); 36 | } 37 | 38 | handleFFMPEGStdout = msg => { 39 | // console.log(msg); 40 | }; 41 | 42 | handleFFMPEGReady = () => { 43 | // console.log("FFMPEG is Ready"); 44 | this.setState({ ffmpegReady: true }); 45 | }; 46 | 47 | handleFFMPEGFileReceived = () => { 48 | // console.log("FFMPEG Received File"); 49 | }; 50 | 51 | handleFFMPEGDone = result => { 52 | this.setState({ 53 | timeRange: { start: 0, end: this.state.timeRange.end } 54 | }); 55 | const videoBlob = arrayBufferToBlob(result[0].data); 56 | setTimeout(() => { 57 | this.decodeVideoFile(videoBlob, () => { 58 | const handler = this.props.onVideoEncode || noop; 59 | handler(result); 60 | this.setState({ 61 | encoding: false, 62 | encoded: true, 63 | encodedVideo: videoBlob 64 | }); 65 | }); 66 | }, 300); 67 | }; 68 | 69 | defaultState = { 70 | decoding: false, 71 | encoding: false, 72 | encoded: false, 73 | playVideo: false, 74 | videoDataURL: "", 75 | videoFrames: [], 76 | isDecoding: false, 77 | timeRange: { start: 5, end: this.props.timeLimit || 15 }, 78 | encodedVideo: null, 79 | playedSeconds: 0, 80 | ffmpegReady: false 81 | }; 82 | 83 | state = this.defaultState; 84 | 85 | updateVideoDataURL = dataURL => this.setState({ videoDataURL: dataURL }); 86 | 87 | updateVideoFrames = frames => this.setState({ videoFrames: frames }); 88 | 89 | updateIsDecoding = state => this.setState({ updateIsDecoding: state }); 90 | updateVideoDuration = duration => 91 | this.setState({ updateVideoDuration: duration }); 92 | 93 | decodeVideoFile = (file, doneCB = noop) => { 94 | this.setState({ decoding: true }); 95 | const webVideo = this.webVideo; 96 | webVideo.videoFile = file; 97 | webVideo 98 | .decode(file) 99 | .then(({ blob, arrayBuffer, dataURL }) => { 100 | this.updateVideoDataURL(dataURL); 101 | const timeRangeStart = this.state.timeRange.start; 102 | const duration = this.webVideo.videoData.duration; 103 | const timeLimit = timeRangeStart + (this.props.timeLimit || 10); 104 | const timeRangeEnd = duration > timeLimit ? timeLimit : duration; 105 | this.setState({ 106 | timeRange: { start: timeRangeStart, end: timeRangeEnd }, 107 | playedSeconds: (timeRangeEnd - timeRangeStart) / 2 + timeRangeStart 108 | }); 109 | this.setState({ decoding: false }); 110 | doneCB(); 111 | }) 112 | .catch(e => console.log(e)); 113 | }; 114 | handleFileSelected = file => { 115 | this.decodeVideoFile(file); 116 | }; 117 | 118 | handleVideoTrim = time => { 119 | this.setState({ timeRange: time }); 120 | }; 121 | handleEncodeVideo = timeRange => { 122 | this.setState({ encoding: true, videoDataURL: "", playVideo: false }); 123 | const timeDifference = timeRange.end - timeRange.start; 124 | // console.log(timeRange); 125 | this.webVideo.trimVideo(timeRange.start, timeDifference); 126 | }; 127 | handlePlayPauseVideo = () => { 128 | const { playVideo } = this.state; 129 | this.setState({ playVideo: !playVideo }); 130 | }; 131 | handlePlayerPause = () => { 132 | // console.log("pause video"); 133 | this.setState({ playVideo: false }); 134 | }; 135 | handlePlayerPlay = () => { 136 | this.setState({ playVideo: true }); 137 | }; 138 | handlePlayerProgress = seconds => { 139 | if (this.state.playVideo) { 140 | this.setState({ playedSeconds: seconds }); 141 | } 142 | }; 143 | handleReselectFile = () => { 144 | this.setState({ 145 | ...this.defaultState, 146 | ffmpegReady: true 147 | }); 148 | }; 149 | VideoPlayerWithTrimmer = ({ showTrimmer }) => { 150 | const { decoding, encoding, encoded, videoDataURL } = this.state; 151 | return ( 152 | <> 153 | {!decoding && !encoding && videoDataURL && ( 154 | 163 | )} 164 | {showTrimmer && ( 165 | 175 | )} 176 | 177 | {!decoding && !encoding && videoDataURL && ( 178 | this.handleDownloadVideo(this.state.encodedVideo)} 180 | canDownload={encoded} 181 | showEncodeBtn={this.props.showEncodeBtn} 182 | onReselectFile={this.handleReselectFile} 183 | onEncode={() => this.handleEncodeVideo(this.state.timeRange)} 184 | onPlayPauseClick={this.handlePlayPauseVideo} 185 | processing={encoding} 186 | playing={this.state.playVideo} 187 | /> 188 | )} 189 | 190 | ); 191 | }; 192 | handleDownloadVideo = encodedVideo => { 193 | const blobURL = readBlobURL(encodedVideo); 194 | download(blobURL, "trimmed.mp4"); 195 | }; 196 | VideoPlayerNoTrimmer = () => { 197 | return ; 198 | }; 199 | render() { 200 | const { 201 | decoding, 202 | encoding, 203 | encoded, 204 | videoDataURL, 205 | ffmpegReady 206 | } = this.state; 207 | return ( 208 |
209 | {!ffmpegReady && ( 210 | 211 | 212 | {this.props.loadingFFMPEGText || "PLEASE WAIT..."} 213 | 214 | )} 215 | {ffmpegReady && encoded && } 216 | {ffmpegReady && !encoded && ( 217 | <> 218 | {!decoding && !encoding && !videoDataURL && ( 219 | 224 | )} 225 | {(decoding || encoding) && ( 226 | 227 | 228 | {encoding ? "ENCODING VIDEO" : "DECODING VIDEO"}... 229 | 230 | )} 231 | 232 | 233 | )} 234 |
235 | ); 236 | } 237 | } 238 | 239 | export const preloadWebVideo = webVideoLoader; 240 | 241 | export default ReactVideoTrimmer; 242 | -------------------------------------------------------------------------------- /src/libs/WebVideo.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | import { readDataURL, arrayBufferToBlob, readArrayBuffer } from "./utils"; 3 | import workerClient from "ffmpeg-webworker"; 4 | import { fromS } from "./formatSeconds"; 5 | 6 | class WebVideo extends EventEmitter { 7 | constructor(videoFile) { 8 | super(); 9 | this.videoFile = videoFile; 10 | this.workerClient = workerClient; 11 | 12 | workerClient.on("onReady", () => this.emit("FFMPEGReady")); 13 | workerClient.on("onStdout", msg => this.emit("FFMPEGStdout", msg)); 14 | workerClient.on("onFileReceived", () => this.emit("FFMPEGFileReceived")); 15 | workerClient.on("onDone", this.handleDoneClientDone); 16 | } 17 | handleDoneClientDone = result => { 18 | // console.log(result); 19 | // if (!this.optimizedVideo) { 20 | // this.optimizedVideo = true; 21 | // const converted = arrayBufferToBlob(result[0].data); 22 | // // console.log(converted); 23 | // workerClient.inputFile = converted; 24 | // setTimeout(this.optimizeVideo, 500); 25 | // } else { 26 | const converted = arrayBufferToBlob(result[0].data); 27 | this.emit("FFMPEGDone", result); 28 | // } 29 | }; 30 | trimVideo = (start = 0, length) => { 31 | const startSeconds = fromS(start, "hh:mm:ss"); 32 | workerClient.runCommand( 33 | `-ss ${startSeconds} -c copy -t ${length} sliced-output.mp4` 34 | ); 35 | }; 36 | 37 | optimizeVideo = () => { 38 | workerClient.runCommand( 39 | `-strict -2 -vcodec libx264 -crf 23 output.mp4`, 40 | 253554432 41 | ); 42 | }; 43 | _videoData = {}; 44 | _videoFile = null; 45 | optimizedVideo = false; 46 | /** 47 | * @type {ArrayBuffer} 48 | */ 49 | _videoBuffer = {}; 50 | 51 | readAsArrayBuffer = async () => { 52 | this._videoBuffer = await readArrayBuffer(this._videoFile); 53 | return this.videoBuffer; 54 | }; 55 | 56 | /** 57 | * @returns {Blob} 58 | * @returns {String} 59 | */ 60 | convertBufferToBlob = buffer => { 61 | let blob = null; 62 | buffer = buffer || this.videoBuffer; 63 | if (buffer.byteLength) { 64 | blob = arrayBufferToBlob(buffer); 65 | } 66 | return blob; 67 | }; 68 | 69 | /** 70 | * @returns {File} 71 | */ 72 | readAsDataURL = async (buffer, blob) => { 73 | buffer = buffer || this.videoBuffer; 74 | blob = blob || this.convertBufferToBlob(buffer); 75 | let dataURL = null; 76 | if (blob) { 77 | dataURL = await readDataURL(blob); 78 | } 79 | return dataURL; 80 | }; 81 | 82 | set videoFile(file) { 83 | if (file && file.type) { 84 | workerClient.inputFile = file; 85 | } 86 | this._videoFile = file; 87 | } 88 | 89 | get videoFile() { 90 | return this._videoFile; 91 | } 92 | 93 | get duration() { 94 | return this._videoData.duration || 0; 95 | } 96 | 97 | get videoData() { 98 | return this._videoData; 99 | } 100 | get videoBuffer() { 101 | return this._videoBuffer; 102 | } 103 | 104 | decode = async file => { 105 | this.videoFile = file; 106 | this.emit("processingFile"); 107 | // Read File As ArrayBuffer 108 | const arrayBuffer = await this.readAsArrayBuffer(); 109 | // convert to dataURL 110 | const dataURL = await this.readAsDataURL(arrayBuffer); 111 | 112 | let videoObjectUrl = URL.createObjectURL(this.videoFile); 113 | let video = document.createElement("video"); 114 | video.src = videoObjectUrl; 115 | while ( 116 | (video.duration === Infinity || isNaN(video.duration)) && 117 | video.readyState < 2 118 | ) { 119 | await new Promise(r => setTimeout(r, 1000)); 120 | video.currentTime = 10000000 * Math.random(); 121 | } 122 | this._videoData = video; 123 | this.emit("processedFile"); 124 | return { dataURL, arrayBuffer, blob: this.convertBufferToBlob() }; 125 | }; 126 | 127 | generateBufferChunks = (arrayBuffer = []) => { 128 | return new Promise((resolve, reject) => { 129 | try { 130 | let chunks = []; 131 | arrayBuffer = arrayBuffer.byteLength ? arrayBuffer : this.videoBuffer; 132 | const typedBuffer = new Uint8Array(arrayBuffer); 133 | const microSec = 1000 * 60; 134 | let startChunk = 0; 135 | for (let i = microSec; i < typedBuffer.byteLength; i += microSec) { 136 | const _buffer = arrayBuffer.slice(startChunk, i); 137 | chunks.push(_buffer); 138 | startChunk = i; 139 | } 140 | resolve(chunks); 141 | } catch (e) { 142 | reject(e); 143 | } 144 | }); 145 | }; 146 | extractFramesFromVideo = (fps = 25) => { 147 | return new Promise(async (resolve, reject) => { 148 | try { 149 | this.emit("extractingFrames"); 150 | let video = this._videoData; 151 | let seekResolve; 152 | video.addEventListener("seeked", async function() { 153 | if (seekResolve) seekResolve(); 154 | }); 155 | let duration = video.duration; 156 | 157 | let canvas = document.createElement("canvas"); 158 | let context = canvas.getContext("2d"); 159 | let [w, h] = [video.videoWidth, video.videoHeight]; 160 | canvas.width = w; 161 | canvas.height = h; 162 | let frames = []; 163 | let interval = 125 / fps; 164 | let currentTime = 0; 165 | 166 | while (currentTime < duration) { 167 | video.currentTime = currentTime; 168 | await new Promise(r => (seekResolve = r)); 169 | 170 | context.drawImage(video, 0, 0, w, h); 171 | let base64ImageData = canvas.toDataURL(); 172 | frames.push(base64ImageData); 173 | 174 | currentTime += interval; 175 | } 176 | this.emit("extractedFrames"); 177 | resolve(frames); 178 | } catch (e) { 179 | reject(e); 180 | } 181 | }); 182 | }; 183 | } 184 | 185 | export default WebVideo; 186 | -------------------------------------------------------------------------------- /src/libs/formatSeconds.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import zeroFill from "zero-fill"; 3 | 4 | /** 5 | * Original from https://www.npmjs.com/package/hh-mm-ss 6 | */ 7 | 8 | // Time units with their corresponding values in miliseconds 9 | const HOUR = 3600000; 10 | const MINUTE = 60000; 11 | const SECOND = 1000; 12 | 13 | const TIME_FORMAT_ERRMSG = "Time format error"; 14 | 15 | // ============================================================================= 16 | // Export functions 17 | // ============================================================================= 18 | 19 | export function fromMs(ms, format = "mm:ss") { 20 | if (typeof ms !== "number" || Number.isNaN(ms)) { 21 | throw new Error("NaN error"); 22 | } 23 | 24 | let absMs = Math.abs(ms); 25 | 26 | let negative = ms < 0; 27 | let hours = Math.floor(absMs / HOUR); 28 | let minutes = Math.floor((absMs % HOUR) / MINUTE); 29 | let seconds = Math.floor((absMs % MINUTE) / SECOND); 30 | let miliseconds = Math.floor(absMs % SECOND); 31 | 32 | return formatTime( 33 | { 34 | negative, 35 | hours, 36 | minutes, 37 | seconds, 38 | miliseconds 39 | }, 40 | format 41 | ); 42 | } 43 | 44 | export function fromS(s, format = "mm:ss") { 45 | if (typeof s !== "number" || Number.isNaN(s)) { 46 | throw new Error("NaN error"); 47 | } 48 | 49 | let ms = s * SECOND; 50 | 51 | return fromMs(ms, format); 52 | } 53 | 54 | export function toMs(time, format = "mm:ss") { 55 | let re; 56 | 57 | if (["mm:ss", "mm:ss.sss", "hh:mm:ss", "hh:mm:ss.sss"].includes(format)) { 58 | re = /^(-)?(?:(\d\d+):)?(\d\d):(\d\d)(\.\d+)?$/; 59 | } else if (format === "hh:mm") { 60 | re = /^(-)?(\d\d):(\d\d)(?::(\d\d)(?:(\.\d+))?)?$/; 61 | } else { 62 | throw new Error(TIME_FORMAT_ERRMSG); 63 | } 64 | 65 | let result = re.exec(time); 66 | if (!result) throw new Error(); 67 | 68 | let negative = result[1] === "-"; 69 | let hours = result[2] | 0; 70 | let minutes = result[3] | 0; 71 | let seconds = result[4] | 0; 72 | let miliseconds = Math.floor((1000 * result[5]) | 0); 73 | 74 | if (minutes > 60 || seconds > 60) { 75 | throw new Error(); 76 | } 77 | 78 | return ( 79 | (negative ? -1 : 1) * 80 | (hours * HOUR + minutes * MINUTE + seconds * SECOND + miliseconds) 81 | ); 82 | } 83 | 84 | export function toS(time, format = "mm:ss") { 85 | let ms = toMs(time, format); 86 | return Math.floor(ms / SECOND); 87 | } 88 | 89 | // ============================================================================= 90 | // Utility functions 91 | // ============================================================================= 92 | 93 | function formatTime(time, format) { 94 | let showMs; 95 | let showSc; 96 | let showHr; 97 | 98 | switch (format.toLowerCase()) { 99 | case "hh:mm:ss.sss": 100 | showMs = true; 101 | showSc = true; 102 | showHr = true; 103 | break; 104 | case "hh:mm:ss": 105 | showMs = !!time.miliseconds; 106 | showSc = true; 107 | showHr = true; 108 | break; 109 | case "hh:mm": 110 | showMs = !!time.miliseconds; 111 | showSc = showMs || !!time.seconds; 112 | showHr = true; 113 | break; 114 | case "mm:ss": 115 | showMs = !!time.miliseconds; 116 | showSc = true; 117 | showHr = !!time.hours; 118 | break; 119 | case "mm:ss.sss": 120 | showMs = true; 121 | showSc = true; 122 | showHr = !!time.hours; 123 | break; 124 | default: 125 | throw new Error(TIME_FORMAT_ERRMSG); 126 | } 127 | 128 | let hh = zeroFill(2, time.hours); 129 | let mm = zeroFill(2, time.minutes); 130 | let ss = zeroFill(2, time.seconds); 131 | let sss = zeroFill(3, time.miliseconds); 132 | 133 | return ( 134 | (time.negative ? "-" : "") + 135 | (showHr 136 | ? showMs 137 | ? `${hh}:${mm}:${ss}.${sss}` 138 | : showSc 139 | ? `${hh}:${mm}:${ss}` 140 | : `${hh}:${mm}` 141 | : showMs 142 | ? `${mm}:${ss}.${sss}` 143 | : `${mm}:${ss}`) 144 | ); 145 | } 146 | -------------------------------------------------------------------------------- /src/libs/preloadWebVideo.js: -------------------------------------------------------------------------------- 1 | import WebVideo from "./WebVideo"; 2 | 3 | export default (opts = {}) => new WebVideo(opts); 4 | -------------------------------------------------------------------------------- /src/libs/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * detect if a file is an video. 3 | * @param {File} file 4 | */ 5 | export const isVideo = file => file.type.indexOf("video") > -1; 6 | 7 | /** 8 | * create range [min .. max] 9 | */ 10 | export const range = (min, max) => 11 | Array.apply(null, { length: max - min + 1 }).map((v, i) => i + min); 12 | 13 | /** 14 | * FileReader via promise 15 | * @param {File} file 16 | * @param {string} dataType 17 | * @return {Promise} 18 | */ 19 | export const readFile = (file, dataType = "ArrayBuffer") => 20 | new Promise((resolve, reject) => { 21 | const reader = new FileReader(); 22 | reader["readAs" + dataType](file); 23 | reader.onload = () => resolve(reader.result); 24 | reader.onerror = err => reject(err); 25 | }); 26 | 27 | /** 28 | * Read File/Blob to ArrayBuffer 29 | * @param {File} file 30 | * @return {Promise} 31 | */ 32 | export const readArrayBuffer = file => readFile(file, "ArrayBuffer"); 33 | 34 | /** 35 | * Read File/Blob to Base64 36 | * @param {File} file 37 | * @return {Promise} 38 | */ 39 | export const readDataURL = file => readFile(file, "DataURL"); 40 | 41 | export const readBlobURL = file => URL.createObjectURL(file); 42 | 43 | export const download = (url, name) => { 44 | const link = document.createElement("a"); 45 | link.href = url; 46 | link.download = name; 47 | link.click(); 48 | }; 49 | 50 | export const rename = (filename, ext, stamp) => 51 | `${filename.replace(/\.\w+$/, "")}${stamp || ""}.${ext}`; 52 | 53 | /** 54 | * format seconds to [minutes, integer, decimal(2)] 55 | * @param {number} seconds 56 | */ 57 | export const formatSeconds = seconds => [ 58 | Math.floor(seconds / 60), 59 | Math.floor(seconds % 60), 60 | Math.round((seconds % 1) * 100) 61 | ]; 62 | 63 | export const leftZero = (num, count) => { 64 | return ("000000" + num).slice(-count); 65 | }; 66 | 67 | export const noop = () => {}; 68 | 69 | export const arrayBufferToBlob = buffer => 70 | new Blob([new Uint8Array(buffer, 0, buffer.byteLength)], { 71 | type: "video/webm", 72 | name: "video.webm" 73 | }); 74 | -------------------------------------------------------------------------------- /src/style.js: -------------------------------------------------------------------------------- 1 | import "./styles/main-container.scss"; 2 | 3 | import "./styles/controls.scss"; 4 | 5 | import "./styles/dragger.scss"; 6 | 7 | import "./styles/file-picker.scss"; 8 | 9 | import "./styles/icon.scss"; 10 | 11 | import "./styles/player.scss"; 12 | 13 | import "./styles/status.scss"; 14 | 15 | import "./styles/trimmer.scss"; 16 | -------------------------------------------------------------------------------- /src/styles/controls.scss: -------------------------------------------------------------------------------- 1 | .btn-reset { 2 | padding: 0; 3 | border: 0; 4 | outline: 0; 5 | background: none; 6 | } 7 | 8 | .rvt-controls-cont { 9 | margin-top: 10px; 10 | text-align: center; 11 | .seconds { 12 | font-size: 12px; 13 | line-height: 36px; 14 | margin-left: 10px; 15 | display: inline-block; 16 | overflow: hidden; 17 | color: #aaa; 18 | } 19 | } 20 | 21 | @keyframes spin { 22 | from { 23 | transform: rotate(0); 24 | } 25 | 26 | to { 27 | transform: rotate(360deg); 28 | } 29 | } 30 | 31 | .rvt-icon-spin { 32 | animation: spin infinite 1s linear; 33 | } 34 | 35 | .rvt-controller-item, 36 | .rvt-file-picker-control { 37 | @extend .btn-reset; 38 | 39 | display: inline-block; 40 | font-size: 16px; 41 | text-align: center; 42 | color: #999; 43 | padding: 10px; 44 | margin-right: 10px; 45 | box-shadow: 0 0 15px rgba(0, 0, 0, 0.1); 46 | cursor: pointer; 47 | 48 | &:hover { 49 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.16); 50 | } 51 | 52 | .rvt-icon { 53 | display: block; 54 | } 55 | } 56 | 57 | .rvt-controller-dropdown { 58 | display: inline-block; 59 | position: relative; 60 | 61 | .rvt-controller-list-wrap { 62 | position: relative; 63 | } 64 | 65 | .rvt-controller-list { 66 | position: absolute; 67 | width: 60px; 68 | top: 0; 69 | left: 0; 70 | visibility: hidden; 71 | opacity: 0; 72 | transition-durvtion: 0.3s; 73 | transition-property: opacity, visibility; 74 | list-style: none; 75 | background: #fff; 76 | box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); 77 | padding: 0; 78 | margin: 0; 79 | z-index: 10; 80 | 81 | a { 82 | @extend .btn-reset; 83 | 84 | display: block; 85 | width: 100%; 86 | text-align: center; 87 | padding: 5px 0; 88 | color: inherit; 89 | font-size: 12px; 90 | cursor: pointer; 91 | 92 | &:hover { 93 | background: #333; 94 | color: #fff; 95 | } 96 | } 97 | } 98 | 99 | &:hover { 100 | .rvt-controller-list { 101 | opacity: 1; 102 | visibility: visible; 103 | } 104 | } 105 | } 106 | 107 | .rvt-controller-clipper { 108 | position: absolute; 109 | width: 100%; 110 | height: 100%; 111 | } 112 | -------------------------------------------------------------------------------- /src/styles/dragger.scss: -------------------------------------------------------------------------------- 1 | .rvt-dragger { 2 | position: absolute; 3 | top: 0; 4 | bottom: 0; 5 | width: 1px; 6 | background: #999; 7 | cursor: col-resize; 8 | 9 | &::after { 10 | content: ""; 11 | position: absolute; 12 | left: -2px; 13 | right: -2px; 14 | top: 0; 15 | bottom: 0; 16 | } 17 | 18 | &:hover { 19 | background: #333; 20 | } 21 | } 22 | 23 | .rvt-drag-current { 24 | background: #038c7f; 25 | } 26 | -------------------------------------------------------------------------------- /src/styles/file-picker.scss: -------------------------------------------------------------------------------- 1 | .rvt-file-picker { 2 | width: 100%; 3 | text-align: center; 4 | padding: 50px; 5 | border: 1px solid #038c7f; 6 | background-color: #ccfdf856; 7 | cursor: pointer; 8 | color: #038c7f; 9 | & input { 10 | visibility: hidden; 11 | position: absolute; 12 | right: 0; 13 | left: 0; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/styles/icon.scss: -------------------------------------------------------------------------------- 1 | .rvt-icon { 2 | width: 1em; 3 | height: 1em; 4 | fill: #038c7f; 5 | vertical-align: middle; 6 | } 7 | -------------------------------------------------------------------------------- /src/styles/main-container.scss: -------------------------------------------------------------------------------- 1 | .btn-reset { 2 | padding: 0; 3 | border: 0; 4 | outline: 0; 5 | background: none; 6 | } 7 | 8 | .rvt-main-container { 9 | > * { 10 | box-sizing: border-box; 11 | } 12 | .rvt-icon-spin { 13 | animation: spin infinite 1s linear; 14 | } 15 | } 16 | @keyframes spin { 17 | from { 18 | transform: rotate(0); 19 | } 20 | 21 | to { 22 | transform: rotate(360deg); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/styles/player.scss: -------------------------------------------------------------------------------- 1 | .rvt-player-cont { 2 | position: relative; 3 | // &::before { 4 | // -webkit-user-drag: none; 5 | // -khtml-user-drag: none; 6 | // -moz-user-drag: none; 7 | // -o-user-drag: none; 8 | // user-drag: none; 9 | 10 | // -webkit-touch-callout: none; /* iOS Safari */ 11 | // -webkit-user-select: none; /* Chrome/Safari/Opera */ 12 | // -khtml-user-select: none; /* Konqueror */ 13 | // -moz-user-select: none; /* Firefox */ 14 | // -ms-user-select: none; /* Internet Explorer/Edge*/ 15 | // user-select: none; /* Non-prefixed version, currently 16 | // not supported by any browser */ 17 | 18 | // content: ""; 19 | // width: 100%; 20 | // height: 100%; 21 | // position: absolute; 22 | // left: 0; 23 | // top: 0; 24 | // } 25 | & .rvt-player-time-range-cont { 26 | margin: 5px; 27 | text-align: center; 28 | & .rvt-player-time-range { 29 | color: rgb(143, 143, 143); 30 | font-size: 14px; 31 | padding: 5px; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/styles/status.scss: -------------------------------------------------------------------------------- 1 | .rvt-status { 2 | width: 100%; 3 | text-align: center; 4 | padding: 50px; 5 | background-color: #ccfdf856; 6 | cursor: pointer; 7 | font-size: 20px; 8 | color: #038c7f; 9 | & input { 10 | visibility: hidden; 11 | position: absolute; 12 | right: 0; 13 | left: 0; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/styles/trimmer.scss: -------------------------------------------------------------------------------- 1 | .rvt-trimmer-cont { 2 | display: block; 3 | width: 100%; 4 | height: 15px; 5 | position: relative; 6 | margin-bottom: 20px; 7 | margin-top: 30px; 8 | background-color: #ccfdf856; 9 | 10 | > * { 11 | box-sizing: border-box; 12 | } 13 | & .rvt-thumb { 14 | position: relative; 15 | display: inline-block; 16 | } 17 | } 18 | .rvt-trimmer { 19 | background: #038c7f; 20 | position: absolute; 21 | top: 0; 22 | right: 0; 23 | width: 100%; 24 | height: 100%; 25 | z-index: 1200; 26 | } 27 | 28 | // cursorColor = #0cf; 29 | .rvt-player-cursor-current { 30 | position: absolute; 31 | font-size: 12px; 32 | top: -22px; 33 | padding: 1px 3px; 34 | text-align: center; 35 | color: #fff; 36 | transform: translate(-50%) scale(0.8); 37 | background: #038c7f; 38 | 39 | .rvt-player-num { 40 | font-family: monospace; 41 | } 42 | 43 | &::after { 44 | content: ""; 45 | position: absolute; 46 | border: 5px solid transparent; 47 | border-top-color: #038c7f; 48 | bottom: -9px; 49 | left: 50%; 50 | margin-left: -5px; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /styleguide.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { createConfig, babel, match, sass, css } = require("webpack-blocks"); 3 | 4 | module.exports = { 5 | title: "react-video-trimmer", 6 | styleguideDir: path.join(__dirname, "docs"), 7 | webpackConfig: createConfig([ 8 | babel(), 9 | match(["*.scss", "*.css", "!*node_modules*"], [css(), sass()]) 10 | ]), 11 | exampleMode: "expand", 12 | usageMode: "expand", 13 | showSidebar: false, 14 | serverPort: 8080, 15 | moduleAliases: { 16 | "react-video-trimmer": path.resolve(__dirname, "./src") 17 | }, 18 | require: [ 19 | path.resolve(__dirname, "styleguide/setup.js"), 20 | path.join(__dirname, "examples/theme.css") 21 | ], 22 | sections: [ 23 | { 24 | name: "", 25 | content: path.resolve(__dirname, "examples/BASE.MD") 26 | } 27 | ] 28 | }; 29 | -------------------------------------------------------------------------------- /styleguide/setup.js: -------------------------------------------------------------------------------- 1 | import "babel-polyfill"; 2 | -------------------------------------------------------------------------------- /testSetup.js: -------------------------------------------------------------------------------- 1 | const jest = require('jest'); 2 | // https://www.npmjs.com/package/jest-dom 3 | require('jest-dom/extend-expect'); 4 | 5 | // TODO: Ignore warnings about act(), it refers to having async side effects updating the state; 6 | // This happens because our async getFilesFromEvent() fn 7 | // See https://github.com/kentcdodds/react-testing-library/issues/281, 8 | // https://github.com/facebook/react/issues/14769 9 | jest.spyOn(console, 'error').mockImplementation(() => jest.fn()); 10 | --------------------------------------------------------------------------------