V2/video capture #682

Merged
merged 3 commits into from Jan 11, 2017
View
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title></title>
+</head>
+<body>
+ <div id="main" class="container"></div>
+ <script type="text/javascript" src="../js/stream.js"></script>
+</body>
+</html>
View
@@ -5,6 +5,8 @@
"description": "「艦これ」をほどよく快適にたのしく遊べるようにするやつです",
"permissions": [
"tabs",
+ "activeTab",
+ "tabCapture",
"webRequest",
"notifications",
"downloads",
@@ -59,6 +61,16 @@
},
"global": true
},
+ "stream": {
+ "description": "動画キャプチャを開始します",
+ "suggested_key": {
+ "windows": "Ctrl+Shift+8",
+ "mac": "Command+Shift+8",
+ "chromeos": "Ctrl+Shift+8",
+ "linux": "Ctrl+Shift+8"
+ },
+ "global": false
+ },
"dashboard": {
"description": "ダッシュボードを開きます",
"suggested_key": {
@@ -90,4 +102,4 @@
}
],
"options_page": "dest/html/options.html"
-}
+}
@@ -6,6 +6,7 @@ const captures = new CaptureService();
import LaunchPosition from "../../Models/LaunchPosition";
import Config from "../../Models/Config";
import Assets from "../../Services/Assets";
+import Streaming from "../../Services/Streaming";
import CaptureWindowURL from "../../Routine/CaptureWindowURL";
@@ -37,3 +38,11 @@ export function OpenDashboard() {
const position = LaunchPosition.find("dashboard");
windows.openDashboard(position);
}
+
+export function OpenStreaming() {
+ Streaming.instance().then(streaming => {
+ let params = new URLSearchParams();
+ params.set("src", streaming.toBlobURL());
+ window.open("/dest/html/stream.html" + "?" + params.toString());
+ });
+}
@@ -0,0 +1,15 @@
+import Streaming from "../../Services/Streaming";
+
+export function StreamStartRecording() {
+ return Streaming.instance().then(streaming => {
+ streaming.startRecording();
+ return Promise.resolve();
+ });
+}
+
+export function StreamStopRecording() {
+ return Streaming.instance().then(streaming => {
+ const url = streaming.stopRecording();
+ return Promise.resolve({url});
+ });
+}
@@ -3,6 +3,7 @@ import * as FrameControllers from "./Frame";
import * as QueuesControllers from "./Queue";
import * as WindowControllers from "./Window";
import * as TwitterControllers from "./Twitter";
+import * as StreamControllers from "./Stream";
import * as HistoryControllers from "./History";
import * as LaunchPositionControllers from "./LaunchPosition";
import * as DamageSnapshotControllers from "./DamageSnapshot";
@@ -15,6 +16,7 @@ const MessageControllers = {
...FrameControllers,
...QueuesControllers,
...TwitterControllers,
+ ...StreamControllers,
...LaunchPositionControllers,
...DamageSnapshotControllers,
...DebugControllers,
@@ -9,6 +9,7 @@ let router = new Router(resolve);
router.on("capture", Controllers.CaptureController);
router.on("mute", Controllers.MuteController);
router.on("dashboard",Controllers.OpenDashboard);
+router.on("stream", Controllers.OpenStreaming);
const CommandRouter = router.listener();
export default CommandRouter;
@@ -20,6 +20,8 @@ router.on("/queues/manual", Controllers.SetQueueManual);
router.on("/twitter/profile", Controllers.TwitterProfile);
router.on("/twitter/auth", Controllers.TwitterAuth);
router.on("/twitter/post_with_image",Controllers.TwitterPostWithImage);
+router.on("/stream/recording/start", Controllers.StreamStartRecording);
+router.on("/stream/recording/stop", Controllers.StreamStopRecording);
router.on("/debug/notification", Controllers.NotificationDebug);
router.on("/launchposition/:update", Controllers.UpdateLaunchPosition);
router.on("/launchposition/dashboard/update", Controllers.UpdateDashboardLaunchPosition);
@@ -0,0 +1,46 @@
+/* global MediaRecorder:false */
+export default class Streaming {
+ static options = {
+ audio:false, video: true,
+ videoConstraints: {
+ mandatory: {
+ chromeMediaSource: "tab",
+ maxWidth: 800, maxHeight: 480
+ }
+ }
+ }
+ static __instance = null
+ static instance() {
+ if (this.__instance == null || !this.__instance.stream.active) {
+ return new Promise(resolve => {
+ chrome.tabCapture.capture(this.options, (stream) => {
+ this.__instance = new this(stream);
+ resolve(this.__instance);
+ });
+ });
+ }
+ return Promise.resolve(this.__instance);
+ }
+ constructor(stream) {
+ this.stream = stream;
+ }
+ toBlobURL() {
+ return URL.createObjectURL(this.stream);
+ }
+ onMediaRecorderStarted(ev) {
+ this.chunks.push(ev.data);
+ }
+ startRecording() {
+ this.chunks = [];
+ this.recorder = new MediaRecorder(this.stream);
+ this.recorder.ondataavailable = this.onMediaRecorderStarted.bind(this);
+ this.recorder.start();
+ }
+ stopRecording() {
+ this.recorder.stop();
+ const blob = new Blob(this.chunks, {"type" : "video/webm; codecs=vp9"});
+ // const blob = new Blob(this.chunks);
+ this.chunks = [];
+ return URL.createObjectURL(blob);
+ }
+}
@@ -0,0 +1,86 @@
+import React, {Component,PropTypes} from "react";
+
+import IconButton from "material-ui/IconButton";
+import FiberManualRecord from "material-ui/svg-icons/av/fiber-manual-record";
+import Stop from "material-ui/svg-icons/av/stop";
+import {red500,grey500} from "material-ui/styles/colors";
+
+import {Client} from "chomex";
+
+class VideoPlayer extends Component {
+ render() {
+ return (
+ <video style={{width: "100%"}} src={this.props.src} autoPlay="true"/>
+ );
+ }
+ static propTypes = {
+ src: PropTypes.string
+ }
+}
+
+class VideoController extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ recording: false,
+ started: 0,
+ };
+ this.client = new Client(chrome.runtime);
+ }
+ getActionButton() {
+ if (this.state.recording) {
+ return (
+ <IconButton iconStyle={{color:grey500}} onClick={this.stopRecording.bind(this)}>
+ <Stop />
+ </IconButton>
+ );
+ }
+ return (
+ <IconButton iconStyle={{color:red500}} onClick={this.startRecording.bind(this)}>
+ <FiberManualRecord />
+ </IconButton>
+ );
+ }
+ startRecording() {
+ this.setState({recording: true, started: Date.now()});
+ this.client.message("/stream/recording/start");
+ }
+ stopRecording() {
+ this.setState({recording: false});
+ this.client.message("/stream/recording/stop").then(({data}) => {
+ if (!data.url) return; // FIXME: とりあえず
+ let a = document.createElement("a");
+ a.href = data.url;
+ a.download = `video_${Date.now()}.webm`;
+ a.click();
+ window.revokeObjectURL(data.url);
+ });
+ }
+ render() {
+ return (
+ <div style={{marginLeft: "12px"}}>
+ {this.getActionButton()}
+ </div>
+ );
+ }
+}
+
+export default class StreamView extends Component {
+ render() {
+ let url = new URL(location.href);
+ return (
+ <div style={{margin:"0 auto", width:"80%"}}>
+ <div style={{display:"flex"}}>
+ <div style={{flex:"2"}}>
+ <VideoPlayer src={url.searchParams.get("src")} />
+ </div>
+ <div style={{flex:"1"}}>
+ </div>
+ </div>
+ <div>
+ <VideoController />
+ </div>
+ </div>
+ );
+ }
+}
@@ -0,0 +1,19 @@
+import React from "react";
+import { render } from "react-dom";
+// import { Provider } from "react-redux"
+import MuiThemeProvider from "material-ui/styles/MuiThemeProvider";
+import getMuiTheme from "material-ui/styles/getMuiTheme";
+import injectTapEventPlugin from "react-tap-event-plugin";
+injectTapEventPlugin();
+
+import { init } from "../global-pollution";
+init(window);
+
+import StreamView from "../../Components/Views/Stream";
+
+render(
+ <MuiThemeProvider muiTheme={getMuiTheme()}>
+ <StreamView />
+ </MuiThemeProvider>,
+ document.getElementById("main")
+);
View
@@ -21,6 +21,7 @@ module.exports = {
dmm: "./src/js/entrypoints/pages/dmm.js",
osapi_dmm :"./src/js/entrypoints/pages/osapi.dmm.js",
capture: "./src/js/entrypoints/pages/capture.js",
+ stream: "./src/js/entrypoints/pages/stream.js",
deckcapture:"./src/js/entrypoints/pages/deckcapture.js",
status: "./src/js/entrypoints/pages/status.js",
dashboard: "./src/js/entrypoints/pages/dashboard.js",