Skip to content

Commit

Permalink
feat(client): implement capturing screen contents
Browse files Browse the repository at this point in the history
  • Loading branch information
hiroppy committed Jun 25, 2019
1 parent 0bcd09d commit 9c98fc0
Show file tree
Hide file tree
Showing 12 changed files with 219 additions and 40 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Just write Markdown and create cool slides.😎
- supports [WebSlides](https://webslides.tv) as Slide UI
- supports [Presentation API](https://developer.mozilla.org/en-US/docs/Web/API/Presentation_API)
- also, Fusuma works even without Presentation API
- supports [Screen Capture API](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Capture_API/Using_Screen_Capture)
- it will help live coding, etc
- 100% SEO
- records your voice
- customizes JavaScript and CSS freely
Expand Down
Binary file added media/capture-screen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions packages/client/assets/style/view.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
@import './variable.css';

/* cannot use before/after for video tags, so should wrap div tags */
.fusuma-screen {
background: #ddd;
box-shadow: 0 0 2px #96bbee;
margin: auto;
position: relative;
height: 100%;
width: 100%;

& > * {
height: 100%;
width: 100%;
}

& > div {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 10;
}

& > video {
}
}
5 changes: 2 additions & 3 deletions packages/client/src/components/AppContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,7 @@ export class AppContainer extends React.Component {
};

onChangeSlideIndex = (currentIndex) => {
if (this.state.isSidebar) {
this.setState({ currentIndex });
}
this.setState({ currentIndex });
};

onRunPresentationMode = () => {
Expand Down Expand Up @@ -180,6 +178,7 @@ export class AppContainer extends React.Component {
hash={this.props.hash}
slides={this.state.slides}
terminate={this.terminate}
currentIndex={this.state.currentIndex}
onChangeSlideIndex={this.onChangeSlideIndex}
/>
)}
Expand Down
12 changes: 4 additions & 8 deletions packages/client/src/components/ContentView/Base.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,15 @@ export class Base extends React.Component {
constructor() {
super();

this.state = {
currentIndex: 0
};

if (!window.slide) {
setTimeout(() => {
window.slide = setupWebSlides();

// for presenter:view
window.slide.el.addEventListener('ws:slide-change', (e) => {
if (this.props.onChangeSlideIndex) {
this.props.onChangeSlideIndex(e.detail.currentSlide0);
}

this.setState({ currentIndex: e.detail.currentSlide0 });
});
}, 0);
}
Expand All @@ -45,9 +41,9 @@ export class Base extends React.Component {

render() {
const {
slides,
slides
// showIndex(webSlides) checks all slides so lazyload can not be used together
lazyload = !process.env.SHOW_INDEX // TODO: fix
// lazyload = !process.env.SHOW_INDEX // TODO: fix
} = this.props;
const articleClass = process.env.IS_VERTICAL ? 'vertical' : undefined;

Expand Down
44 changes: 24 additions & 20 deletions packages/client/src/components/ContentView/Host.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,16 @@ export default class Host extends React.PureComponent {

this.state = {
usedAudio: false,
currentSlide: 0,
isOpenTimeline: false,
status: 'prepare', // prepare, start, stop
isEmptyRecordedTimeline: true
};

document.onkeyup = (e) => {
if (e.key === 'ArrowLeft') {
this.changeCurrentSlide(Math.max(0, this.state.currentSlide - 1));
this.changeCurrentSlide(Math.max(0, this.props.currentIndex - 1));
} else if (e.key === 'ArrowRight') {
this.changeCurrentSlide(Math.min(this.slides.length - 1, this.state.currentSlide + 1));
this.changeCurrentSlide(Math.min(this.slides.length - 1, this.props.currentIndex + 1));
}
};
}
Expand All @@ -74,15 +73,17 @@ export default class Host extends React.PureComponent {
terminate = () => {
try {
this.props.terminate();
if (this.presentationController) this.presentationController.terminate();
if (this.presentationController) {
this.presentationController.terminate();
}
this.presentationController = null;
} catch (e) {
console.error(e);
}
};

changeCurrentSlide = (num) => {
if (this.recordedStartedTime !== 0) {
if (this.state.status === 'start') {
const time = new Date().getTime() - this.recordedStartedTime;
const prevItem = this.recordedTimeline.slice(-1)[0];

Expand All @@ -91,14 +92,14 @@ export default class Host extends React.PureComponent {
time,
timeStr: `${formatTime(time)} (+${formatTime(time - prevItem.time)})`,
event: 'changed',
title: `Moved to the ${num + 1} slide from the ${this.state.currentSlide + 1} slide.`,
title: `Moved to the ${num + 1} slide from the ${num} slide.`,
Slide: this.slides[num].slide,
color: '#3498db',
Icon: <FaCaretRight size="22" />
});
}

this.setState({ currentSlide: num });
this.props.onChangeSlideIndex(num);
this.presentationController.changePage(num);
};

Expand All @@ -111,18 +112,18 @@ export default class Host extends React.PureComponent {
this.recordedTimeline.length === 0 ? 0 : new Date().getTime() - this.recordedStartedTime;

this.recordedTimeline.push({
slideNum: this.state.currentSlide + 1,
slideNum: this.props.currentIndex + 1,
time,
timeStr: formatTime(time),
event: 'started',
title: `Started from the ${this.state.currentSlide + 1} slide.`,
Slide: this.slides[this.state.currentSlide].slide,
title: `Started from the ${this.props.currentIndex + 1} slide.`,
Slide: this.slides[this.props.currentIndex].slide,
color: '#6fba1c',
Icon: <FaCaretDown />
});

if (this.state.usedAudio) {
this.webrtc.start();
this.webrtc.startRecording();
this.audioUrl = null;
}

Expand All @@ -133,17 +134,17 @@ export default class Host extends React.PureComponent {
const time = new Date().getTime() - this.recordedStartedTime;

this.recordedTimeline.push({
slideNum: this.state.currentSlide + 1,
slideNum: this.props.currentIndex + 1,
time,
timeStr: formatTime(time),
event: 'stopped',
title: `Stopped at the ${this.state.currentSlide + 1} slide.`,
title: `Stopped at the ${this.props.currentIndex + 1} slide.`,
color: '#e9546b',
Icon: <FaCaretUp />
});

if (this.state.usedAudio) {
this.audioUrl = await this.webrtc.stop();
this.audioUrl = await this.webrtc.stopRecording();
}

this.setState({ status: 'stop' });
Expand All @@ -168,6 +169,7 @@ export default class Host extends React.PureComponent {
if (!this.webrtc) {
try {
this.webrtc = new WebRTC();
this.webrtc.setupRecording();
this.setState({ usedAudio: true });
} catch (e) {
alert(e);
Expand All @@ -177,7 +179,7 @@ export default class Host extends React.PureComponent {

disposeRecording = () => {
if (this.webrtc) {
this.webrtc.dispose();
this.webrtc.disposeRecording();
this.webrtc = null;
}

Expand All @@ -192,7 +194,7 @@ export default class Host extends React.PureComponent {
// usedAudio && status === 'start'
// mic
render() {
const index = this.state.currentSlide;
const { currentIndex } = this.props;

return (
<div className="host-container">
Expand All @@ -202,23 +204,25 @@ export default class Host extends React.PureComponent {
<div className="host-left-box">
<div className="host-note">
{this.slides && (
<pre dangerouslySetInnerHTML={{ __html: this.slides[index].fusumaProps.note }} />
<pre
dangerouslySetInnerHTML={{ __html: this.slides[currentIndex].fusumaProps.note }}
/>
)}
</div>
</div>
<div className="host-right-box">
<div className="host-slide-layer">
<h2>Current</h2>
<iframe
src={`${this.slideUrl.replace(/slide=(\d?)/, `slide=${index + 1}`)}`}
src={`${this.slideUrl.replace(/slide=(\d?)/, `slide=${currentIndex + 1}`)}`}
width="100%"
height="100%"
/>
</div>
<div className="host-slide-layer">
<h2>Next</h2>
<iframe
src={`${this.slideUrl.replace(/slide=(\d?)/, `slide=${index + 2}`)}`}
src={`${this.slideUrl.replace(/slide=(\d?)/, `slide=${currentIndex + 2}`)}`}
width="100%"
height="100%"
/>
Expand All @@ -245,7 +249,7 @@ export default class Host extends React.PureComponent {
/>
<span className="current-slide-num">
{/* TODO: fix */}
{`${index + 1}`.padStart(2, '0')} / {`${this.slides.length}`.padStart(2, '0')}
{`${currentIndex + 1}`.padStart(2, '0')} / {`${this.slides.length}`.padStart(2, '0')}
</span>
<FaHistory
onClick={this.openTimeline}
Expand Down
73 changes: 70 additions & 3 deletions packages/client/src/components/ContentView/View.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,89 @@
import React from 'react';
import { Base } from './Base';
import { Receiver as PresentationReceiver } from '../../presentationMode/Receiver';
import { WebRTC } from '../../utils/webrtc';
import '../../../assets/style/view.css';

export default class View extends React.PureComponent {
constructor() {
super();

this.webrtc = null;
this.currentVideoTag = null;
this.currentLayer = null;
this.presentationReceiver = new PresentationReceiver();

this.presentationReceiver.onChangePage((pageNum) => {
if (window.slide) {
window.slide.goToSlide(pageNum);

// stop capturing
if (this.webrtc && this.currentVideoTag) {
this.stopCapturing(this.currentVideoTag);
}
}
});
}

componentDidMount() {
this.listenVideoTags();
}

async listenVideoTags(id) {
const videoContainers = document.querySelectorAll('#webslides .fusuma-screen');

if (videoContainers === null) return;

// need to active the view screen if want to capture the screen
videoContainers.forEach((container) => {
const [overlay, video] = container.children;

video.addEventListener('click', async () => {
if (!this.webrtc) {
const stream = await this.startCapturing();

video.srcObject = stream;
video.play();
this.currentVideoTag = video;
this.currentLayer = overlay;
this.currentLayer.style.opacity = 0;

// Click on browser UI stop sharing button
//https://stackoverflow.com/questions/25141080/how-to-listen-for-stop-sharing-click-in-chrome-desktopcapture-api
stream.getVideoTracks()[0].onended = () => {
this.stopCapturing(video);
};
}
});
});
}

startCapturing = async () => {
if (!this.webrtc) {
this.webrtc = new WebRTC();

return await this.webrtc.startCapturing({
video: {
displaySurface: 'monitor'
}
});
} else {
throw new Error('Capturing has already run.');
}
};

stopCapturing = (elem) => {
if (this.webrtc && elem) {
elem.pause();
this.webrtc.stopCapturing(elem);
this.currentLayer.style.opacity = 1;
this.webrtc = null;
this.currentLayer = null;
this.currentVideoTag = null;
}
};

render() {
return (
<Base slides={this.props.slides} lazyload={false} currentIndex={this.props.currentIndex} />
);
return <Base slides={this.props.slides} hash={this.props.hash} />;
}
}
1 change: 1 addition & 0 deletions packages/client/src/components/Timeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export class Timeline extends React.PureComponent {
const index = this.calcIndex(this.audioRef.currentTime * 1000);

// first index is unnecessary
// firefox doesn't work
elems[index].scrollIntoView();
});
}
Expand Down
Loading

0 comments on commit 9c98fc0

Please sign in to comment.