import './Uploader.css' import DateTime from 'date-and-time' import PromiseIpc from 'electron-promise-ipc' import Fs from 'fs' import * as IPFSHTTPClient from 'ipfs-http-client' import randomstring from 'randomstring' import React, { useEffect, useRef, useState } from 'react' import { Button, Card, Col, Form, ProgressBar, Row, Tab, Tabs } from 'react-bootstrap' import { NotificationManager } from 'react-notifications' import { IPFS_HOST } from '../../common/constants' import { bytesAsString, millisecondsAsString, secondsAsString, } from '../../common/utils/unit-conversion.functions' import DefaultThumbnail from '../assets/img/default-thumbnail.jpg' import LoadingMessage from '../components/LoadingMessage' import { FormUtils } from '../renderer_utils' import { AccountService } from '../services/account.service' export function UploaderView() { const videoUpload = useRef<any>() const thumbnailUpload = useRef<any>() const thumbnailPreview = useRef('') const publishForm = useRef() const hwaccelOption = useRef() const ipfs = useRef<any>() const [logData, setLogData] = useState([]) const [videoSourceFile, setVideoSourceFile] = useState() const [encodingInProgress, setEncodingInProgress] = useState(false) const [progress, setProgress] = useState<any>({}) const [statusInfo, setStatusInfo] = useState<any>({ progress: {} }) const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState('') const [videoInfo, setVideoInfo] = useState<any>({ path: null, size: 0, cid: null, language: '', duration: null, }) const [thumbnailInfo, setThumbnailInfo] = useState({ path: null, size: 0, cid: null, }) const [startTime, setStartTime] = useState<number>() const [endTime, setEndTime] = useState<number>(0) const [publishReady, setPublishReady] = useState(false) const [blockedGlobalMessage, setBlockedGlobalMessage] = useState('') useEffect(() => { ipfs.current = IPFSHTTPClient.create({ host: IPFS_HOST }) }, []) const caluclatePercentage = () => { return progress.percent / statusInfo.nstages + statusInfo.stage * (100 / statusInfo.nstages) } const normalizeSize = () => { const size = videoInfo.size + thumbnailInfo.size return bytesAsString(size) } const compileVideoCid = async () => { const videoCid = videoInfo.cid if (thumbnailInfo.cid) { const obj = await ipfs.current.object.stat(thumbnailInfo.cid) console.log(obj) console.log(thumbnailInfo) const output = await ipfs.current.object.patch.addLink(videoCid, { name: thumbnailInfo.path, size: thumbnailInfo.size, cid: thumbnailInfo.cid, }) console.log(output) return output.toString() } return videoCid } /** * Note: example metadata https://hiveblocks.com/hive-181335/@taskmaster4450/tqxwimhy */ const publish = async () => { const videoCid = await compileVideoCid() const formData = FormUtils.formToObj(new FormData(publishForm.current)) let tags: string[] = [] if (formData.tags) { tags = formData.tags.replace(/\s/g, '').split(',') } console.log(`thumbnail info`, thumbnailInfo) const sourceMap = [] if (thumbnailInfo.path) { sourceMap.push({ type: 'thumbnail', url: `ipfs://${videoCid}/${thumbnailInfo.path}`, }) } if (videoInfo) { sourceMap.push({ type: 'video', url: `ipfs://${videoCid}/${videoInfo.path}`, format: 'm3u8', }) } const permlink = `speak-${randomstring .generate({ length: 8, charset: 'alphabetic', }) .toLowerCase()}` // console.log(permlink) console.log(`source map`) console.log(sourceMap) // console.log(videoInfo) // console.log(typeof formData.description) // console.log(videoCid) setBlockedGlobalMessage('Publishing') const filesize = videoInfo.size + thumbnailInfo.size console.log(`formdata is `, formData) formData.title = formData.title || 'no form data' formData.description = formData.description || 'no form data' console.log(`publish form is `, publishForm.current) try { const [reflink] = await AccountService.postComment({ accountType: 'hive', title: formData.title, body: formData.description, permlink, tags, json_metadata: { title: formData.title, description: formData.description, tags, sourceMap, filesize, created: new Date(), lang: videoInfo.language, video: { duration: videoInfo.duration, }, app: '3speak/app-beta', type: '3speak/video', }, }) setTimeout(() => { location.hash = `#/watch/${reflink}` setBlockedGlobalMessage('done') }, 15000) } catch (error) { console.error(`Error in postComment operation ${error.message}`) throw error } } const handleVideoSelect = async (e) => { const file = e.target.files[0] setVideoSourceFile(file.path) setLogData([...logData, `Selected: ${videoInfo.path}`]) } const handleThumbnailSelect = async (e) => { console.log(`handling thumbnail selectr`) const file = e.target.files[0] const imgblob = URL.createObjectURL(file) const size = file.size console.log(`uploading file with size ${size}`) thumbnailPreview.current = imgblob const fileDetails = { path: e.target.files[0].name, content: e.target.files[0], } const ipfsOut = await ipfs.current.add(fileDetails, { pin: false }) console.log(`setting thumbnail info to cid`, ipfsOut.cid.toString()) setThumbnailInfo({ size, path: `thumbnail.${file.type.split('/')[1]}`, cid: ipfsOut.cid.toString(), }) } const handleStartEncode = async (event) => { event.preventDefault() if (videoSourceFile === null) { NotificationManager.error('No video source file selected') return } if (!Fs.existsSync(videoSourceFile)) { NotificationManager.error('Source file does not exist') return } setEncodingInProgress(true) setStartTime(new Date().getTime()) setEndTime(null) const jobInfo = (await PromiseIpc.send('encoder.createJob', { sourceUrl: videoSourceFile, profiles: [ { name: '1080p', size: '1920x1080', }, { name: '720p', size: '1080x720', }, { name: '480p', size: '720x480', }, ], options: { hwaccel: hwaccelOption.current !== '' && hwaccelOption.current !== 'none' ? hwaccelOption.current : undefined, }, } as any)) as any NotificationManager.success('Encoding Started.') let savePct = 0 const progressTrack = async () => { const status = (await PromiseIpc.send('encoder.status', jobInfo.id)) as any console.log(`Encoder status: `, status) setProgress(status.progress || {}) setStatusInfo(status) const val = caluclatePercentage() const diffPct = val - savePct savePct = val const pctPerSec = diffPct / 3 const totalTimeRemaining = (100 - val) / pctPerSec setEstimatedTimeRemaining(secondsAsString(totalTimeRemaining)) setEndTime(new Date().getTime()) } const pid = setInterval(progressTrack, 3000) void progressTrack() const encodeOutput = (await PromiseIpc.send('encoder.getjoboutput', jobInfo.id)) as any console.log(`got encode output`) console.log(encodeOutput) setVideoInfo({ size: encodeOutput.size, cid: encodeOutput.ipfsHash, path: encodeOutput.path, duration: Number(DateTime.parse(encodeOutput.duration, 'hh:mm:ss.SS', true)) / 1000, }) clearInterval(pid) setEncodingInProgress(false) setEstimatedTimeRemaining(null) setEndTime(new Date().getTime()) setPublishReady(true) NotificationManager.success('Encoding complete.') } if (blockedGlobalMessage) { return ( <LoadingMessage loadingMessage={blockedGlobalMessage} subtitle="Note: you will need to keep the app open for your video to play for other users. A process called 'shunting' will be released in the future to relieve this issue." /> ) } return ( <div style={{ width: '95%', marginRight: 'auto', marginLeft: 'auto' }}> <Row style={{ marginTop: '1.45rem' }}> <div> <div className="d-table-cell align-middle card dz-clickable" onClick={() => videoUpload.current.click()} style={{ width: '4000px', textAlign: 'center', height: '150px', fontSize: '16px', fontWeight: 'bold', cursor: 'pointer', }} > Drop a file or click to start the upload <br /> <p> Selected: <kbd>{videoSourceFile}</kbd> </p> <input accept="video/*" type="file" id="video" className="d-none" ref={videoUpload} onChange={handleVideoSelect} /> </div> </div> </Row> <Row style={{ marginTop: '15px' }}> <Col xl={6} sm={12} style={{ paddingLeft: '0px' }}> <div className="card" style={{ padding: '10px' }}> <Form ref={publishForm.current}> <Form.Group> <Form.Label>Title</Form.Label> <Form.Control type="text" name="title"></Form.Control> </Form.Group> <Form.Group> <Form.Label>Description</Form.Label> <textarea className="form-control" name="description"></textarea> </Form.Group> <Form.Group> <Form.Label>Tags</Form.Label> <Form.Control type="text" name="tags"></Form.Control> <small className="text-muted"> Separate multiple tags with a <kbd>,</kbd>{' '} </small> </Form.Group> <Form.Group> <Form.Label>Language</Form.Label> <select disabled={false} name="language" className="form-control mb-4"> <option selected={false} value="en"> English </option> <option value="de">Deutsch</option> <option value="fr">Français</option> <option value="es">Español</option> <option value="nl">Nederlands</option> <option value="ko">한국어</option> <option value="ru">русский</option> <option value="hu">Magyar</option> <option value="ro">Română</option> <option value="cs">čeština</option> <option value="pl">Polskie</option> <option value="in">bahasa Indonesia</option> <option value="bn">Bangla</option> <option value="it">Italian</option> </select> </Form.Group> <span className="form-check mb-3"> <input type="checkbox" className="form-check-input" id="nsfwContent" name="nsfwContent" /> <label className="form-check-label" htmlFor="nsfwContent"> Content is graphic and/or NSFW <span className="text-danger"> Warning: you should check this option if your content is <a href="https://en.wikipedia.org/wiki/Not_safe_for_work">NSFW</a>. </span> </label> </span> <Form.Group> <Form.Label>Thumbnail</Form.Label> <div></div> <img src={thumbnailPreview.current || DefaultThumbnail} style={{ width: '720px', aspectRatio: '16/9', cursor: 'pointer', }} alt="" onClick={() => thumbnailUpload.current.click()} /> <input accept="image/*" type="file" id="thumbnail_input" className="d-none" ref={thumbnailUpload} onChange={handleThumbnailSelect} /> <p>Click the thumbnail to change it</p> <p>Recommended 5MB. Ideally 1280px×720px.</p> </Form.Group> <Button onClick={handleStartEncode}>Start Encode</Button> <Button style={{ marginLeft: '5px' }} onClick={publish} disabled={encodingInProgress || !publishReady} > Publish </Button> </Form> </div> </Col> <Col style={{ paddingRight: '0px', paddingLeft: '0px' }}> <Card> <Card.Header>Encoder status</Card.Header> <Card.Body> <Card.Text>This area will show live encoding statistics</Card.Text> <Button style={{ marginBottom: '5px' }} variant="primary"> FPS: {progress.currentFps} </Button>{' '} <br /> <Button variant="primary">Video Size: {normalizeSize()}</Button> <ProgressBar style={{ display: encodingInProgress ? '' : 'none', }} striped variant="success" now={caluclatePercentage()} label={progress.percent ? `${Math.round(caluclatePercentage())}%` : null} /> <div style={{ display: encodingInProgress ? '' : 'none', }} > Time Remaining:{' '} {estimatedTimeRemaining !== 'NaNns' ? estimatedTimeRemaining : 'Calculating'} </div> <div style={{ display: endTime ? '' : 'none' }}> Total Time (so far):{' '} {endTime !== 0 ? millisecondsAsString(endTime - startTime) : 'Calculating'} </div> </Card.Body> </Card> <div className="card" style={{ marginTop: '15px' }}> <div className="card-header"> <h5>Control Panel</h5> </div> <Tabs style={{ background: 'white' }} defaultActiveKey="encode"> <Tab style={{ background: 'white', padding: '10px' }} eventKey="encode" title="Encode Settings" > <Form.Group> <Form.Label> <strong>Format</strong> </Form.Label> <select style={{ width: '6em' }} disabled={encodingInProgress} id="language" className="form-control mb-4" > <option selected={false} value="hls"> HLS </option> </select> </Form.Group> <Form.Group> <Form.Label> <strong>Hardware Accel</strong> </Form.Label> <Form.Text> Use hardware acceleration to speed up video encode. Not available on all systems, results may vary. </Form.Text> <select style={{ width: '6em' }} ref={hwaccelOption} disabled={encodingInProgress} id="language" className="form-control mb-4" > <option selected={false} value="none"> none </option> <option value="qsv">QSV</option> <option value="nvenc">nvenc</option> </select> </Form.Group> </Tab> <Tab eventKey="info" title="Info" style={{ padding: '10px' }}> <Form.Group> <Form.Label>Video IpfsPath</Form.Label> <Form.Control type="text" name="vidoeHash" disabled value={videoInfo.cid} ></Form.Control> </Form.Group> <Form.Group> <Form.Label>Thumbnail IpfsPath</Form.Label> <Form.Control type="text" name="thumbnailHash" disabled value={thumbnailInfo.cid} ></Form.Control> </Form.Group> <Form.Group> <Form.Label>Total Size</Form.Label> <Form.Control style={{ width: '15%' }} type="text" name="videoSize" value={normalizeSize()} disabled ></Form.Control> </Form.Group> </Tab> {/*<Tab eventKey="networks" title="Networks"> <Table striped bordered hover size="sm"> <thead> <tr> <th>Enabled</th> <th>ID</th> <th>Username</th> </tr> </thead> <tbody> <tr> <td><Form.Check/></td> <td>Hive</td> <td>vaultec</td> </tr> </tbody> </Table> </Tab>*/} <Tab eventKey="log" title="Log" style={{ padding: '10px' }}> <textarea disabled className="form-control" value={(() => logData.join('\n'))()} ></textarea> </Tab> </Tabs> </div> </Col> </Row> </div> ) }