Initial Push
This commit is contained in:
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
3
API/DB.js
Normal file
3
API/DB.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
DB: 'mongodb://localhost:27017/videoStreamer' // Connection to DB
|
||||||
|
}
|
||||||
91
API/index.js
Normal file
91
API/index.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
NodeJS Video Streamer - index.js
|
||||||
|
By: Johnathon Slightham
|
||||||
|
*/
|
||||||
|
const express = require("express");
|
||||||
|
const app = express();
|
||||||
|
const fs = require("fs");
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
const cors = require('cors');
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const config = require('./DB.js');
|
||||||
|
const postRoute = require('./post.route');
|
||||||
|
const fileUpload = require('express-fileupload');
|
||||||
|
|
||||||
|
printWelcome();
|
||||||
|
// Connect to database
|
||||||
|
mongoose.Promise = global.Promise;
|
||||||
|
mongoose.connect(config.DB, { useNewUrlParser: true, useUnifiedTopology: true}).then(
|
||||||
|
() => {console.log('Connected to database') },
|
||||||
|
err => { console.log('Can not connect to the database: '+ err)}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Express
|
||||||
|
app.use(cors());
|
||||||
|
app.use(bodyParser.urlencoded({extended: true}));
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
app.use(fileUpload());
|
||||||
|
app.use('/posts', postRoute);
|
||||||
|
app.listen(8000, function () {
|
||||||
|
console.log("Express listening on port 8000");
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get thumbnail for video with :id
|
||||||
|
*/
|
||||||
|
app.get("/thumbnails/:id", (req, res) => {
|
||||||
|
let id = req.params.id;
|
||||||
|
if(id)
|
||||||
|
res.sendFile(__dirname + "/thumbnails/" + id + ".jpg");
|
||||||
|
else
|
||||||
|
res.send(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Stream video with :id
|
||||||
|
*/
|
||||||
|
app.get("/video2/:id", (req, res) => {
|
||||||
|
|
||||||
|
let id = req.params.id; // ID of video to be streamed
|
||||||
|
|
||||||
|
// Check if the header includes range
|
||||||
|
const range = req.headers.range;
|
||||||
|
if (!range) {
|
||||||
|
res.status(400).send("Missing range header");
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoPath = "videos/" + id + ".mp4"; // path of the video
|
||||||
|
const videoSize = fs.statSync("videos/" + id + ".mp4").size; // size of the video
|
||||||
|
|
||||||
|
// Parse Range
|
||||||
|
const CHUNK_SIZE = 5 ** 6; // Half megabyte
|
||||||
|
let start = Number(range.replace(/\D/g, ""));
|
||||||
|
let end = Math.min(start + CHUNK_SIZE, videoSize - 1);
|
||||||
|
|
||||||
|
// Create headers
|
||||||
|
const contentLength = end - start + 1;
|
||||||
|
const headers = {
|
||||||
|
"Content-Range": `bytes ${start}-${end}/${videoSize}`,
|
||||||
|
"Accept-Ranges": "bytes",
|
||||||
|
"Content-Length": contentLength,
|
||||||
|
"Content-Type": "video/mp4",
|
||||||
|
};
|
||||||
|
|
||||||
|
// HTTP Status 206 for Partial Content
|
||||||
|
res.writeHead(206, headers);
|
||||||
|
|
||||||
|
// create video read stream for this particular chunk
|
||||||
|
const videoStream = fs.createReadStream(videoPath, { start, end });
|
||||||
|
|
||||||
|
// Stream the video chunk to the client
|
||||||
|
videoStream.pipe(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function printWelcome(){
|
||||||
|
console.log("-----------------------------------");
|
||||||
|
console.log("NodeJS-Video-Streamer by jslightham");
|
||||||
|
console.log("Version 1.0");
|
||||||
|
console.log("-----------------------------------");
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
1927
API/package-lock.json
generated
Normal file
1927
API/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
API/package.json
Normal file
21
API/package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "open-video-stream",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "nodemon index.js"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"express-fileupload": "^1.2.0",
|
||||||
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
|
"handbrake-js": "^5.0.2",
|
||||||
|
"mongoose": "^5.11.8",
|
||||||
|
"nodemon": "^2.0.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
35
API/post.model.js
Normal file
35
API/post.model.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
NodeJS-Video-Streamer - post.model.js
|
||||||
|
|
||||||
|
*/
|
||||||
|
// Schema for a video post
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
|
let Post = new Schema({
|
||||||
|
title: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
comments: {
|
||||||
|
type: Array
|
||||||
|
},
|
||||||
|
transcoding: {
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
progress: {
|
||||||
|
type: Number
|
||||||
|
},
|
||||||
|
eta:{
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
likes:{
|
||||||
|
type: Number
|
||||||
|
},
|
||||||
|
},{
|
||||||
|
collection: 'posts'
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = mongoose.model('Post', Post);
|
||||||
182
API/post.route.js
Normal file
182
API/post.route.js
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const postRoutes = express.Router();
|
||||||
|
const hbjs = require('handbrake-js')
|
||||||
|
const fs = require("fs");
|
||||||
|
const ffmpeg = require('fluent-ffmpeg');
|
||||||
|
|
||||||
|
// Require Post model in our routes module
|
||||||
|
let Post = require('./post.model');
|
||||||
|
|
||||||
|
/*
|
||||||
|
Route for uploading a video
|
||||||
|
First save the video to API/toTranscode/id.mp4, then use ffmpeg to get a screenshot
|
||||||
|
and save the screenshot to API/thumbnails/id.jpg. Then, use handbrake to transcode
|
||||||
|
the video to a compressed mp4 for easier streaming.
|
||||||
|
*/
|
||||||
|
postRoutes.route('/upload').post((req, res) => {
|
||||||
|
// Create the post object and initiate the values
|
||||||
|
let post = new Post();
|
||||||
|
console.log(req.body);
|
||||||
|
post.title = req.body.title;
|
||||||
|
post.description = req.body.description;
|
||||||
|
post.transcoding = true;
|
||||||
|
post.progress = 0;
|
||||||
|
post.likes = 0;
|
||||||
|
post.comments = [];
|
||||||
|
|
||||||
|
// Check if a file was included in the uplaod
|
||||||
|
if (!req.files) {
|
||||||
|
return res.status(500).send({ msg: "file is not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const myFile = req.files.file;
|
||||||
|
|
||||||
|
// Place the file into toTranscode directory
|
||||||
|
myFile.mv(`${__dirname}/toTranscode/${post._id}.mp4`, err => {
|
||||||
|
|
||||||
|
// If there was an error, print it to the console
|
||||||
|
if (err) {
|
||||||
|
console.log(err)
|
||||||
|
return res.status(500).send({ msg: "Error occured" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the post to the databse
|
||||||
|
post.save();
|
||||||
|
|
||||||
|
// Take a 480p screenshot of the video 50% through, and save it to the
|
||||||
|
// thumbnails folder.
|
||||||
|
ffmpeg(`${__dirname}/toTranscode/${post._id}.mp4`)
|
||||||
|
.screenshots({
|
||||||
|
timestamps: ['50%'],
|
||||||
|
filename: `${post._id}.jpg`,
|
||||||
|
folder: `${__dirname}/thumbnails`,
|
||||||
|
size: '704x480'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Transcode the video in the toTranscode directory to an mp4 using Very Fast 1080p30 preset, and save to videos directory
|
||||||
|
hbjs.spawn({ input: `${__dirname}/toTranscode/${post._id}.mp4`, output: `${__dirname}/videos/${post._id}.mp4`, preset: "Very Fast 1080p30" })
|
||||||
|
.on('error', err => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Save the progress and eta to the database
|
||||||
|
.on('progress', prog => {
|
||||||
|
post.progress = prog.percentComplete;
|
||||||
|
|
||||||
|
// if the eta is empty, leave it the same
|
||||||
|
if (prog.eta) {
|
||||||
|
post.eta = prog.eta;
|
||||||
|
}
|
||||||
|
post.save()
|
||||||
|
})
|
||||||
|
|
||||||
|
// When done transcoding, delete the old file, and change status of transcoding to false
|
||||||
|
.on('end', () => {
|
||||||
|
post.transcoding = false;
|
||||||
|
post.save
|
||||||
|
|
||||||
|
// Delete file in toTranscode directory
|
||||||
|
fs.unlink(`${__dirname}/toTranscode/${post._id}.mp4`, (err) => {
|
||||||
|
// Log deletion error to console
|
||||||
|
if (err) {
|
||||||
|
console.error(err)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return res.send({ name: myFile.name, path: `/${post._id}` });
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
Route to add a like to video with id of :id
|
||||||
|
*/
|
||||||
|
postRoutes.route('/like/:id').get(function (req, res) {
|
||||||
|
let id = req.params.id;
|
||||||
|
|
||||||
|
// Find the post that has the id
|
||||||
|
Post.findById(id, function (err, post) {
|
||||||
|
if (err) {
|
||||||
|
res.json(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a like and save to database
|
||||||
|
post.likes++;
|
||||||
|
post.save();
|
||||||
|
res.json(post);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Route to add a comment to the video with an id of :id
|
||||||
|
*/
|
||||||
|
postRoutes.route('/postComment/:id').post(function (req, res) {
|
||||||
|
let id = req.params.id;
|
||||||
|
|
||||||
|
// Find the post that has the id
|
||||||
|
Post.findById(id, function (err, post) {
|
||||||
|
if (err) {
|
||||||
|
res.json(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push comment on to the comments array, and save it
|
||||||
|
post.comments.push(req.body.comment);
|
||||||
|
post.save();
|
||||||
|
res.json(post);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get the entry for the video with id of :id
|
||||||
|
*/
|
||||||
|
postRoutes.route('/vinfo/:id').get(function (req, res) {
|
||||||
|
let id = req.params.id;
|
||||||
|
|
||||||
|
// Find the video with id
|
||||||
|
Post.findById(id, function (err, post) {
|
||||||
|
if (err) {
|
||||||
|
res.json(err);
|
||||||
|
}
|
||||||
|
res.json(post);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Search for all videos that contain query in their title
|
||||||
|
*/
|
||||||
|
postRoutes.route('/search').post(function (req, res) {
|
||||||
|
let query = req.body.query;
|
||||||
|
|
||||||
|
// Get all posts
|
||||||
|
Post.find(function (err, posts) {
|
||||||
|
if (err) {
|
||||||
|
res.json(err);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Filter for posts that have a title containing query
|
||||||
|
posts = posts.filter(post => {
|
||||||
|
return post.title.toLowerCase().includes(query.toLowerCase());
|
||||||
|
});
|
||||||
|
res.json(posts);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get info for all videos
|
||||||
|
*/
|
||||||
|
postRoutes.route('/').get(function (req, res) {
|
||||||
|
Post.find(function (err, posts) {
|
||||||
|
if (err) {
|
||||||
|
res.json(err);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
res.json(posts);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = postRoutes;
|
||||||
24
README.md
Normal file
24
README.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# nodejs-video-streamer
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
```
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||||
5
babel.config.js
Normal file
5
babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/cli-plugin-babel/preset'
|
||||||
|
]
|
||||||
|
}
|
||||||
12324
package-lock.json
generated
Normal file
12324
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
package.json
Normal file
48
package.json
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "nodejs-video-streamer",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.21.0",
|
||||||
|
"bootstrap-vue": "^2.21.1",
|
||||||
|
"core-js": "^3.6.5",
|
||||||
|
"vue": "^2.6.11",
|
||||||
|
"vue-axios": "^3.2.0",
|
||||||
|
"vue-router": "^3.4.9",
|
||||||
|
"vue-video-player": "^5.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||||
|
"@vue/cli-service": "~4.5.0",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"eslint": "^6.7.2",
|
||||||
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
|
"vue-cli-plugin-vuetify": "~2.0.8",
|
||||||
|
"vue-template-compiler": "^2.6.11"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:vue/essential",
|
||||||
|
"eslint:recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"parser": "babel-eslint"
|
||||||
|
},
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not dead"
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
17
public/index.html
Normal file
17
public/index.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
62
src/App.vue
Normal file
62
src/App.vue
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<div id="content">
|
||||||
|
<b-navbar toggleable="lg" type="dark" style="background-color: #1e2934">
|
||||||
|
<b-navbar-brand href="/">NodeJS Video Streamer</b-navbar-brand>
|
||||||
|
|
||||||
|
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
|
||||||
|
|
||||||
|
<b-collapse id="nav-collapse" is-nav>
|
||||||
|
<b-navbar-nav>
|
||||||
|
<b-nav-item to="/">Home</b-nav-item>
|
||||||
|
<b-nav-item to="Upload">Upload</b-nav-item>
|
||||||
|
</b-navbar-nav>
|
||||||
|
|
||||||
|
<!-- Right aligned nav items -->
|
||||||
|
<b-navbar-nav class="ml-auto">
|
||||||
|
<b-nav-form>
|
||||||
|
<form action="/" method="GET">
|
||||||
|
<b-form-input size="sm" class="mr-sm-2" placeholder="Search" name="q" style="background-color: #505f6d; border: none"></b-form-input>
|
||||||
|
<b-button size="sm" class="my-2 my-sm-0" type="submit">Search</b-button>
|
||||||
|
</form>
|
||||||
|
</b-nav-form>
|
||||||
|
|
||||||
|
|
||||||
|
</b-navbar-nav>
|
||||||
|
</b-collapse>
|
||||||
|
</b-navbar>
|
||||||
|
<transition
|
||||||
|
name="fade"
|
||||||
|
mode="out-in"
|
||||||
|
>
|
||||||
|
<router-view></router-view>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'app'
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#app {
|
||||||
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
background-color: #32404e;
|
||||||
|
height: 100%;
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
html{
|
||||||
|
background-color: #343a40;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
79
src/components/Home.vue
Normal file
79
src/components/Home.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<div style="margin-top: 25px; text-align: center">
|
||||||
|
<div v-if="!posts.length" style="text-align: center">No videos found!</div>
|
||||||
|
|
||||||
|
<div class="card vid-card" v-for="post in posts" :key="post._id" style="">
|
||||||
|
<h5 class="card-header" style="background-color: #1e2934">
|
||||||
|
{{ post.title }}
|
||||||
|
</h5>
|
||||||
|
<img
|
||||||
|
class="card-img-top"
|
||||||
|
v-bind:src="uri + '/thumbnails/' + post._id"
|
||||||
|
alt="Video Thumbnail"/>
|
||||||
|
<div class="card-body" style="background-color: #404e5b">
|
||||||
|
<p class="card-text">{{ post.description }}</p>
|
||||||
|
<b-link v-bind:to="'/video?id=' + post._id" class="btn btn-lg btn-primary">Watch</b-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
posts: [],
|
||||||
|
uri: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.uri = this.$apiIp;
|
||||||
|
// If there is a query, call the query function
|
||||||
|
if (!this.$route.query.q)
|
||||||
|
this.getAllPosts();
|
||||||
|
else
|
||||||
|
this.getQueryPosts();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// Get the data for all videos
|
||||||
|
getAllPosts() {
|
||||||
|
this.axios.get(this.$apiIp + "/posts/").then((res) => {
|
||||||
|
this.posts = res.data;
|
||||||
|
this.posts.map(post => {
|
||||||
|
if(post.description.length > 100){
|
||||||
|
post.description = post.description.substring(0, 100);
|
||||||
|
}
|
||||||
|
return post;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// Get the data for the query
|
||||||
|
getQueryPosts() {
|
||||||
|
let msg = {};
|
||||||
|
msg.query = this.$route.query.q;
|
||||||
|
this.axios.post(this.$apiIp + "/posts/search", msg).then((res) => {
|
||||||
|
this.posts = res.data;
|
||||||
|
this.posts.map(post => {
|
||||||
|
if(post.description.length > 175){
|
||||||
|
post.description.substring(0, 175);
|
||||||
|
}
|
||||||
|
return post;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.vid-card{
|
||||||
|
width: 18rem;
|
||||||
|
height: 27rem;
|
||||||
|
color: white;
|
||||||
|
margin-left: 25px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
margin-top: 25px;
|
||||||
|
border: none;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
130
src/components/Upload.vue
Normal file
130
src/components/Upload.vue
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<template>
|
||||||
|
<center>
|
||||||
|
<div class="card upload-card">
|
||||||
|
<h5 class="card-header" style="background-color: #1e2934">
|
||||||
|
Upload Video
|
||||||
|
</h5>
|
||||||
|
<div class="card-body" style="background-color: #404e5b">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="exampleFormControlInput1"><h5>Video Title</h5></label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="exampleFormControlInput1"
|
||||||
|
style="color: white; background-color: #505f6d; border: none"
|
||||||
|
v-model="post.title"
|
||||||
|
placeholder="Title"/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="exampleFormControlTextarea1"><h5>Description</h5></label>
|
||||||
|
<textarea
|
||||||
|
class="form-control"
|
||||||
|
id="exampleFormControlTextarea1"
|
||||||
|
rows="3"
|
||||||
|
v-model="post.description"
|
||||||
|
style="color: white; background-color: #505f6d; border: none"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="file-upload" style="margin-bottom: 15px">
|
||||||
|
<input type="file" @change="onFileChange" />
|
||||||
|
<div class="progress">
|
||||||
|
<div
|
||||||
|
class="progress-bar"
|
||||||
|
role="progressbar"
|
||||||
|
v-bind:style="'width:' + progress + '%;'"
|
||||||
|
v-bind:aria-valuenow="progress"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100">
|
||||||
|
{{ progress }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<button @click="onUploadFile" class="upload-button btn btn-primary">
|
||||||
|
Upload file
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</center>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedFile: {},
|
||||||
|
post: {},
|
||||||
|
progress: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onFileChange(e) {
|
||||||
|
if (
|
||||||
|
e.target.files[0].name
|
||||||
|
.substring(e.target.files[0].name.lastIndexOf("."))
|
||||||
|
.toLowerCase() == ".mp4" ||
|
||||||
|
e.target.files[0].name
|
||||||
|
.substring(e.target.files[0].name.lastIndexOf("."))
|
||||||
|
.toLowerCase() == ".mov" ||
|
||||||
|
e.target.files[0].name
|
||||||
|
.substring(e.target.files[0].name.lastIndexOf("."))
|
||||||
|
.toLowerCase() == ".wmv" ||
|
||||||
|
e.target.files[0].name
|
||||||
|
.substring(e.target.files[0].name.lastIndexOf("."))
|
||||||
|
.toLowerCase() == ".flv" ||
|
||||||
|
e.target.files[0].name
|
||||||
|
.substring(e.target.files[0].name.lastIndexOf("."))
|
||||||
|
.toLowerCase() == ".avi" ||
|
||||||
|
e.target.files[0].name
|
||||||
|
.substring(e.target.files[0].name.lastIndexOf("."))
|
||||||
|
.toLowerCase() == ".webm" ||
|
||||||
|
e.target.files[0].name
|
||||||
|
.substring(e.target.files[0].name.lastIndexOf("."))
|
||||||
|
.toLowerCase() == ".mkv" ||
|
||||||
|
e.target.files[0].name
|
||||||
|
.substring(e.target.files[0].name.lastIndexOf("."))
|
||||||
|
.toLowerCase() == ".m4v"
|
||||||
|
) {
|
||||||
|
this.selectedFile = e.target.files[0];
|
||||||
|
} else {
|
||||||
|
alert("Please only upload .mp4 or .mov files!");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onUploadFile() {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("title", this.post.title);
|
||||||
|
formData.append("description", this.post.description);
|
||||||
|
formData.append("file", this.selectedFile); // appending file
|
||||||
|
console.log(formData);
|
||||||
|
// sending file to the backend
|
||||||
|
this.axios
|
||||||
|
.post(this.$apiIp + "/posts/upload", formData, {
|
||||||
|
onUploadProgress: (ProgressEvent) => {
|
||||||
|
let progress = Math.round(
|
||||||
|
(ProgressEvent.loaded / ProgressEvent.total) * 100
|
||||||
|
);
|
||||||
|
this.progress = progress;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.upload-card {
|
||||||
|
width: 28rem;
|
||||||
|
margin-top: 25px;
|
||||||
|
background-color: #404e5b;
|
||||||
|
}
|
||||||
|
.progress{
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-right: 20px;
|
||||||
|
background-color: #505f6d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
187
src/components/Video.vue
Normal file
187
src/components/Video.vue
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
<template>
|
||||||
|
<div class="wholePage">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-5 center-middle">
|
||||||
|
<div class="card" style="background-color: #404e5b;">
|
||||||
|
<h5 class="card-header" style="background-color: #1e2934">Video Information</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title video-title">{{vInfo.title}}</h2>
|
||||||
|
<p class="card-text">{{vInfo.description}}</p>
|
||||||
|
<h3 v-if="vInfo.transcoding">Transcoding Progress: {{vInfo.eta}}</h3>
|
||||||
|
<div class="progress" v-if="vInfo.transcoding">
|
||||||
|
<div class="progress-bar" role="progressbar" style="width: 25%" v-bind:style="'width: '+ vInfo.progress + '%'" v-bind:aria-valuenow="vInfo.progress" aria-valuemin="0" aria-valuemax="100"></div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<form v-on:submit.prevent="like">
|
||||||
|
<button href="#" class="btn btn-primary" style="width: 100%">Like - {{vInfo.likes}}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="card" style="background-color: #404e5b;">
|
||||||
|
<h5 class="card-header" style="background-color: #1e2934">Recent Videos</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<center><span v-for="post in posts" :key="post._id" style="margin-right: 15px;"><a v-bind:href="'/video?id='+ post._id"><img v-bind:src="uri + '/thumbnails/'+post._id" width="150px" style="margin-top: 15px;"></a></span></center>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-7 center-middle">
|
||||||
|
<br>
|
||||||
|
<div class="video">
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<div class="card" style="background-color: #404e5b; width: 75%;">
|
||||||
|
<h5 class="card-header" style="background-color: #1e2934">Video</h5>
|
||||||
|
<img v-if="vInfo.transcoding" v-bind:src="uri+'/thumbnails/'+ vInfo._id" class="unloaded-thumbnail">
|
||||||
|
|
||||||
|
<video
|
||||||
|
v-if="!vInfo.transcoding"
|
||||||
|
id="my-video"
|
||||||
|
class="video-js vjs-theme-forest"
|
||||||
|
controls
|
||||||
|
fluid="true"
|
||||||
|
v-bind:poster="uri +'/thumbnails/' + id"
|
||||||
|
data-setup="{}"
|
||||||
|
preload="metadata">
|
||||||
|
<source v-bind:src="uri +'/video2/' + id" type="video/mp4" />
|
||||||
|
<p class="video-js vjs-theme-forest">
|
||||||
|
To view this video please enable JavaScript, and consider upgrading to a
|
||||||
|
web browser that <a href="https://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a>
|
||||||
|
</p>
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="card" style="background-color: #404e5b;">
|
||||||
|
<h5 class="card-header" style="background-color: #1e2934">Comments</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="media pt-3" v-for="com in vInfo.comments" :key="com">
|
||||||
|
<p class="media-body pb-3 mb-0 small lh-125 border-bottom border-gray">
|
||||||
|
{{com}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="exampleFormControlTextarea1"><h5>Create New</h5></label>
|
||||||
|
<textarea class="form-control" id="exampleFormControlTextarea1" rows="3" style="color: white; background-color: #505f6d; border: none;" v-model="comment"></textarea>
|
||||||
|
<br>
|
||||||
|
<button class="upload-button btn btn-primary" @click="submitComment">Post</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import 'https://unpkg.com/video.js@7/dist/video-js.min.css';
|
||||||
|
@import 'https://unpkg.com/@videojs/themes@1/dist/forest/index.css';
|
||||||
|
|
||||||
|
.user_name{
|
||||||
|
font-size:14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.comments-list .media{
|
||||||
|
border-bottom: 1px dotted #ccc;
|
||||||
|
}
|
||||||
|
.wholePage{
|
||||||
|
margin-left: 25px;
|
||||||
|
margin-right: 25px;
|
||||||
|
margin-top: 50px;
|
||||||
|
|
||||||
|
}
|
||||||
|
.center-middle{
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
.video-title{
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100% !important;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.unloaded-thumbnail{
|
||||||
|
max-height: 50vh;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Similarly, you can also introduce the plugin resource pack you want to use within the component
|
||||||
|
// import 'some-videojs-plugin'
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
posts: [],
|
||||||
|
vInfo: {},
|
||||||
|
id: "",
|
||||||
|
comment: "",
|
||||||
|
uri: "",
|
||||||
|
playerOptions: {
|
||||||
|
// videojs options
|
||||||
|
|
||||||
|
language: 'en',
|
||||||
|
playbackRates: [0.7, 1.0, 1.5, 2],
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
sources: [{
|
||||||
|
type: "video/mp4",
|
||||||
|
src: this.$apiIp + "/video2/" + this.$route.query.id,
|
||||||
|
}],
|
||||||
|
poster: this.$apiIp + "/thumbnails/" + this.$route.query.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.uri = this.$apiIp;
|
||||||
|
this.vInfo.transcoding = true;
|
||||||
|
this.getVideoInfo();
|
||||||
|
this.getAllPosts();
|
||||||
|
this.id = this.$route.query.id;
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getVideoInfo(){
|
||||||
|
this.axios.get(this.$apiIp + "/posts/vinfo/" + this.$route.query.id).then((res) =>{
|
||||||
|
this.vInfo = res.data;
|
||||||
|
if(this.vInfo.transcoding){
|
||||||
|
this.sleep(3000);
|
||||||
|
console.log(this.$router.name);
|
||||||
|
if(this.$route.name == 'video'){
|
||||||
|
this.getVideoInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
sleep(ms) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getAllPosts(){
|
||||||
|
this.axios.get(this.$apiIp + "/posts/").then((res) =>{
|
||||||
|
this.posts = res.data.reverse().slice(0, 4);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
like(){
|
||||||
|
this.axios.get(this.$apiIp + "/posts/like/" + this.$route.query.id).then((res) =>{
|
||||||
|
this.vInfo = res.data;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitComment(){
|
||||||
|
let msg = {};
|
||||||
|
msg.comment = this.comment;
|
||||||
|
this.axios.post(this.$apiIp + "/posts/postComment/" + this.$route.query.id, msg).then((res) =>{
|
||||||
|
this.vInfo = res.data;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
43
src/main.js
Normal file
43
src/main.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||||
|
import VueAxios from 'vue-axios';
|
||||||
|
import axios from "axios";
|
||||||
|
import VueRouter from 'vue-router';
|
||||||
|
import BootstrapVue from 'bootstrap-vue'
|
||||||
|
|
||||||
|
Vue.prototype.$apiIp = "http://127.0.0.1:8000"
|
||||||
|
|
||||||
|
Vue.use(VueRouter);
|
||||||
|
Vue.use(VueAxios, axios);
|
||||||
|
Vue.use(BootstrapVue);
|
||||||
|
|
||||||
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
|
import Video from './components/Video.vue';
|
||||||
|
import Upload from './components/Upload.vue';
|
||||||
|
import Home from './components/Home.vue';
|
||||||
|
import VueVideoPlayer from 'vue-video-player'
|
||||||
|
Vue.use(VueVideoPlayer);
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
name: 'video',
|
||||||
|
path: '/video',
|
||||||
|
component: Video
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'upload',
|
||||||
|
path: '/upload',
|
||||||
|
component: Upload
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'home',
|
||||||
|
path: '/',
|
||||||
|
component: Home
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const router = new VueRouter({ mode: 'history', routes: routes});
|
||||||
|
|
||||||
|
new Vue(Vue.util.extend({ router }, App)).$mount('#app');
|
||||||
Reference in New Issue
Block a user