Skip to content

Instantly share code, notes, and snippets.

@amad-person
Last active December 3, 2018 08:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save amad-person/7928dd3a51ebfff0ec9f3dbf52ba2ab2 to your computer and use it in GitHub Desktop.
Save amad-person/7928dd3a51ebfff0ec9f3dbf52ba2ab2 to your computer and use it in GitHub Desktop.
Extracts from Find And Seek's Front End for CS3281

Find And Seek Client

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:

App.js

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;

FileDrop.js

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;

VideoPlayer.js

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;

QueryForm.js

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;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment