A CTRL-F for video. This document contains relevant information and code extracts from the front end of the project.
Tech Stack:
- Client was built using ReactJS.
- Styled using
reactstrap
, a Bootstrap port for ReactJS.
Links:
- DevPost: Project Post
- Code: GitHub
Main entry point for Find And Seek's client. Displays file selection component and then shows video player after video has been processed.
import React, { Component } from 'react';
import Header from '../../components/Header/Header';
import FileDrop from '../FileDrop/FileDrop';
import VideoPlayer from '../VideoPlayer/VideoPlayer';
import { Button } from 'reactstrap';
import './App.css';
import {
BrowserRouter as Router,
Route,
Link,
Switch,
} from 'react-router-dom';
class App extends Component {
constructor(props) {
super(props);
this.state = {
doneUploading: false,
showingPlayer: false,
files: [],
source_url: ''
};
}
// receive file object from FileDrop child component
dataFromFileDrop = (fileData) => {
this.setState({
doneUploading: fileData.doneUploading,
showingPlayer: false,
files: fileData.files,
source_url: fileData.source_url
});
};
// click handler to trigger video processing
processVideo = () => {
this.setState({
showingPlayer: true
});
};
// starting point of main application
render() {
return (
<Router>
<div className="App">
<Header/>
<Switch>
<Route
exact
path="/"
render={(props) => <FileDrop {...props} dataFromFileDrop={this.dataFromFileDrop}/>}
/>
<Route
path="/upload"
render={(props) => <FileDrop {...props} dataFromFileDrop={this.dataFromFileDrop}/>}
/>
<Route
path="/process"
render={(props) => <VideoPlayer {...props} source_url={this.state.source_url}/>}
/>
<Route
render={(props) => <FileDrop {...props} dataFromFileDrop={this.dataFromFileDrop}/>}
/>
</Switch>
{
!this.state.showingPlayer ?
this.state.doneUploading ?
<div>
<Button tag={Link} to="/process" color="success" size="lg"
onClick={this.processVideo}>
Query
</Button>
</div>:
null :
null
}
</div>
</Router>
);
}
}
export default App;
Component that handles file selection from user and passes the file to parent (App.js) once received.
Also uploads file to backend.
Uses react-dropzone
to handle file selection and axios
to handle requests.
import React, { Component } from 'react';
import Dropzone from 'react-dropzone';
import request from 'superagent';
import axios from 'axios';
import './FileDrop.css'
class FileDrop extends Component {
constructor(props) {
super(props);
this.state = { files: [] };
}
// accept file and send to server
onDrop = (files) => {
this.setState({
files
});
// API endpoint http://54.255.249.117:3001/upload
let data = {}
data[files[0].name]= files[0];
const url = 'http://54.255.249.117:3001/upload';
// build POST request to endpoint
const req = request.post(url);
// attach file to POST request
req.attach(files[0].name, files[0]);
// process response
req.end((err, res)=>{
// send data to parent component
const fileData = {
files: this.state.files,
source_url: this.state.files[0].preview,
doneUploading: true
};
console.log("res",res);
console.log("err",err);
// send to parent
this.props.dataFromFileDrop(fileData);
});
};
// get human-readable file size
getFileSize = (size) => {
let sizeExt = ['bytes', 'KB', 'MB', 'GB'], i = 0;
while (size > 900) {
size /= 1000;
i++;
}
return '' + (Math.round(size * 100) / 100) + ' ' + sizeExt[i];
};
// display file drop area
render() {
return (
<div className="FileDrop">
<Dropzone
accept="video/mp4"
className="Dropzone"
activeClassName="DropzoneActive"
rejectClassName="DropzoneReject"
multiple={false}
onDrop={this.onDrop.bind(this)}
>
{({ isDragAccept, isDragReject, acceptedFiles, rejectedFiles }) => {
if (acceptedFiles.length || rejectedFiles.length) {
return `Please wait for the query button to appear!`;
}
if (isDragAccept) {
return "You can upload this file!";
}
if (isDragReject) {
return "This file can't be uploaded.";
}
return "Drag and drop a video file here!";
}}
</Dropzone>
<div className="DroppedFileInfo">{this.state.files.map(f => <p key={f.name}>{f.name} ~ {this.getFileSize(f.size)}</p>)}</div>
</div>
);
};
}
export default FileDrop;
Displays video in a media player with the basic controls (pause, play, etc).
Shows a search box for user queries. Displays the results of queries after timestamps have been received from backend.
Uses moment
to process and format timestamps.
import React, { Component } from 'react';
import { Container, Row, Col, Button, CardDeck, Card, Alert } from 'reactstrap';
import QueryForm from "../QueryForm/QueryForm";
import moment from 'moment';
class VideoPlayer extends Component {
constructor(props) {
super(props);
this.state = {
audioSeekTimes: [],
videoSeekTimes: []
};
}
// stores search results from query
dataFromQuery = (dataResults) => {
this.setState({
audioSeekTimes: dataResults.audioResponse,
videoSeekTimes: dataResults.videoResponse
});
console.log('State for audio seek times', this.state.audioSeekTimes);
console.log('State for video seek times', this.state.videoSeekTimes);
};
// seek to given time in video
seek(time) {
this.refs.videoRef.currentTime = time;
}
render() {
const paddingStyle = {
padding: '20px'
};
// render buttons for audio search timestamps
let audioSeekButtons = (
<Alert color="danger">No results found!</Alert>
);
if (this.state.audioSeekTimes.length !== 0) {
audioSeekButtons = (
<CardDeck>
{ this.state.audioSeekTimes.map((time) => {
return (
<Card>
<Button color="primary" onClick={() => this.seek(Math.floor(time))}>
{moment().startOf('day').seconds(time).format('HH:mm:ss')}
</Button>
</Card>
)})
}
</CardDeck>
);
}
// render buttons for video search timestamps
let videoSeekButtons = (
<Alert color="danger">No results found!</Alert>
);
if (this.state.videoSeekTimes.length !== 0) {
videoSeekButtons = (
<CardDeck>
{ this.state.videoSeekTimes.map((time) => {
return (
<Card>
<Button color="primary" onClick={() => this.seek(Math.floor(time))}>
{moment().startOf('day').seconds(time).format('HH:mm:ss')}
</Button>
</Card>
)})
}
</CardDeck>
);
}
// display video, search box, and search results
return (
<Container className="VideoPlayer">
<Row style={paddingStyle}>
<Col>
<video ref="videoRef" className="Player" width="80%" maxheight="100%" controls>
<source src={this.props.source_url} type={"video/mp4"}/>
</video>
</Col>
</Row>
<Row style={paddingStyle}>
<Col>
<QueryForm dataFromQuery={this.dataFromQuery}/>
</Col>
</Row>
<p style={paddingStyle}>Audio Search Results</p>
{audioSeekButtons}
<p style={paddingStyle}>Video Search Results</p>
{videoSeekButtons}
</Container>
)
}
}
export default VideoPlayer;
Form component that handles user queries. Calls the backend with the query and passes the data to parent (VideoPlayer.js).
import React, { Component } from 'react';
import { Container, Row, Col, Button, Form, FormGroup, Input } from 'reactstrap';
import request from 'superagent';
class QueryForm extends Component {
constructor(props) {
super(props);
this.state = {
value: ''
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
console.log('A query was submitted: ' + this.state.value);
// API deployed at http://54.255.249.117:3001/query
const url = 'http://54.255.249.117:3001/query';
const responseParsed = {
audioResponse: [],
videoResponse: []
};
// call API with query
request.get(url)
.set('API_KEY', 'sampleKey1')
.set('queryString', this.state.value)
.then((res) => {
console.log('query form audio seek times', res.body.audioResponse);
console.log('query form video seek times', res.body.videoResponse);
responseParsed.audioResponse = res.body.audioResponse;
responseParsed.videoResponse = res.body.videoResponse;
this.props.dataFromQuery(responseParsed);
});
event.preventDefault();
}
// display search box
render() {
return (
<Form onSubmit={this.handleSubmit}>
<Container>
<Row>
<Col md="11">
<FormGroup>
<Input type="text" name="query" id="query"
placeholder="Enter some text to find and seek!"
value={this.state.value}
onChange={this.handleChange}
/>
</FormGroup>
</Col>
<Col md="1">
<Button color="primary" type="submit">Find!</Button>
</Col>
</Row>
</Container>
</Form>
);
}
}
export default QueryForm;