├── .editorconfig ├── .gitignore ├── README.md ├── app.html ├── assets ├── css │ ├── _variables.scss │ └── global.scss ├── icons │ ├── file.png │ ├── link.png │ └── youtube.svg └── images │ ├── background.jpg │ ├── background_1.jpg │ ├── background_2.jpg │ └── background_3.jpg ├── components ├── AudioBar.vue ├── Avatar.vue ├── Controls.vue ├── Info.vue ├── JoinRoom.vue ├── Modal.vue ├── Participant.vue ├── Player.vue ├── RoomSetup.vue ├── Spinner.vue ├── VideoStopped.vue └── modals │ ├── EditRoom.vue │ ├── Invite.vue │ └── VideoLinkModal.vue ├── layouts └── landing.vue ├── middleware └── README.md ├── nuxt.config.js ├── package-lock.json ├── package.json ├── pages ├── _room.vue ├── createroom.vue ├── index.vue └── joinroom.vue ├── plugins ├── ping.js ├── socket-io.js ├── srt-webvtt.js └── vue-plyr.js ├── screenshots ├── screenshot_1.png ├── screenshot_2.png ├── screenshot_3.png └── screenshot_4.png ├── static ├── favicon.ico ├── file_icon.png ├── sample.vtt └── video_icon.png └── store └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | /logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE / Editor 81 | .idea 82 | 83 | # Service worker 84 | sw.* 85 | 86 | # macOS 87 | .DS_Store 88 | 89 | # Vim swap files 90 | *.swp 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🍿 Movie Night 2 | 3 |

4 | Version 5 | 6 | License: MIT 7 | 8 |

9 | 10 | > Web app to play local video files and youtube videos in sync with your friends for a great movie night! 11 | 12 | ## ⭐️ [Demo](https://movienight.ecris.in/) 13 | 14 | ## 🏙 Screenshots 15 | 16 | ![Player](./screenshots/screenshot_1.png) 17 | ![Landing page](./screenshots/screenshot_2.png) 18 | ![Creating room](./screenshots/screenshot_3.png) 19 | ![Waiting for host](./screenshots/screenshot_4.png) 20 | 21 | ## 💿 Usage 22 | 23 | ```sh 24 | # install dependencies 25 | $ yarn install 26 | 27 | # serve with hot reload at localhost:3000 28 | $ yarn dev 29 | 30 | # build for production and launch server 31 | $ yarn build 32 | $ yarn start 33 | 34 | # generate static project 35 | $ yarn generate 36 | ``` 37 | 38 | ## ‼️ Note 39 | 40 | This is the frontend of the web app. You can get the server [here](https://github.com/emanuelchristo/movie-night-server) 41 | 42 | ## 🌳 Environment Variables 43 | 44 | On deploying set `SERVER_URL` env var to the server's url 45 | 46 | ## 🪜 Steps 47 | 48 | 1. Create a room ➕ 49 | 2. Add a name and thumbnail 🌆 50 | 3. Add video 🎬 51 | 4. Invite your friends 🕺 52 | 5. Chill 🍿 53 | 54 | ## 💎 Features 55 | 56 | 🔮 Beautiful UI 57 | 🔥 Fast realtime updates 58 | ⌛️ No refreshes needed 59 | 📺 Floating video 60 | 📄 Subtitles 61 | 62 | ## ❓ FAQs 63 | 64 | #### 1. What can I do with this? 65 | 66 | > Watch video (local files and youtube) in sync with your friends 67 | 68 | #### 2. Does that mean all my friends should have the same video file? 69 | 70 | > Yes, if you want to watch videos from you device. You can also watch youtube videos by just adding its link 71 | 72 | #### 3. Do I have to login to use this? 73 | 74 | > Absolutely no. All you have to give is a nickname - whatever you prefer 75 | 76 | ## 🚗 Roadmap 77 | 78 | - Add voice chat 79 | - Add text chat 80 | - Responsive UI 81 | - Add more host controls 82 | - [Random Movie](https://github.com/emanuelchristo/random-movie) integration 83 | - Movie brainstorming board 84 | - Streaming 85 | - Social media of cinephiles 86 | 87 | ## 🧑‍💻 Author 88 | 89 | Emanuel Christo 90 | 91 | ## 🔗 Links 92 | 93 | [![github](https://img.shields.io/badge/github-000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/emanuelchristo) 94 | [![linkedin](https://img.shields.io/badge/linkedin-0A66C2?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/emanuelchristo/) 95 | [![twitter](https://img.shields.io/badge/instagram-f76623?style=for-the-badge&logo=instagram&logoColor=white)](https://instagram.com/emanuel.christo) 96 | 97 | ## 📄 License 98 | 99 | MIT 100 | 101 | ## Show your support 102 | 103 | Give a ⭐️ if this project helped you! 104 | 105 | --- 106 | -------------------------------------------------------------------------------- /app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ HEAD }} 6 | 7 | 8 | 9 | 16 | 17 | 18 | 19 | {{ APP }} 20 | 21 | 22 | -------------------------------------------------------------------------------- /assets/css/_variables.scss: -------------------------------------------------------------------------------- 1 | $grid-line-color: rgba(#fff, 0.06); 2 | 3 | $trans-short: ease-out all 0.1s; 4 | $trans-med: ease-out all 0.3s; 5 | 6 | @mixin gradient { 7 | background: rgb(190, 0, 235); 8 | background: linear-gradient( 9 | 150deg, 10 | rgba(190, 0, 235, 1) 0%, 11 | rgba(78, 0, 183, 1) 100% 12 | ); 13 | } 14 | 15 | $menu-bg-color: rgb(19, 20, 26); 16 | 17 | @mixin red { 18 | background: rgb(223, 0, 0); 19 | background: linear-gradient(150deg, rgb(233, 0, 0) 0%, rgb(144, 5, 5) 100%); 20 | } 21 | 22 | .red { 23 | @include red; 24 | } 25 | 26 | @mixin audio-gradient { 27 | background: rgb(0, 224, 58); 28 | background: linear-gradient( 29 | 90deg, 30 | rgba(0, 224, 58, 1) 0%, 31 | rgba(255, 233, 0, 1) 62%, 32 | rgba(242, 1, 1, 1) 100% 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /assets/css/global.scss: -------------------------------------------------------------------------------- 1 | @import "~/assets/css/_variables.scss"; 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | font-family: "Inter", sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | text-rendering: optimizeLegibility; 11 | } 12 | 13 | :root { 14 | --plyr-color-main: linear-gradient( 15 | 150deg, 16 | rgba(190, 0, 235, 1) 0%, 17 | rgba(78, 0, 183, 1) 100% 18 | ); 19 | --plyr-video-background: rgba(255, 255, 255, 1); 20 | 21 | --plyr-badge-border-radius: 5px; 22 | --plyr-badge-background: rgb(197, 0, 0); 23 | --plyr-badge-text-color: #fff; 24 | 25 | --plyr-captions-text-color: rgb(255, 187, 0); 26 | --plyr-captions-background: rgba(0, 0, 0, 0.459); 27 | 28 | --plyr-control-spacing: 12px; 29 | // --plyr-video-control-background-hover: rgba(255, 255, 255, 0.3); 30 | 31 | --plyr-menu-background: rgb(19, 20, 26); 32 | --plyr-menu-color: rgba(255, 255, 255, 0.8); 33 | --plyr-menu-shadow: 0px 0px 2px rgba(255, 255, 255, 0.5); 34 | 35 | --plyr-video-progress-buffered-background: rgba(255, 255, 255, 0.4); 36 | --plyr-range-track-height: 3px; 37 | --plyr-range-fill-background: rgb(116, 6, 206); 38 | --plyr-video-range-track-background: rgba(255, 255, 255, 0.3); 39 | 40 | --plyr-tooltip-background: rgb(19, 20, 26); 41 | --plyr-tooltip-color: rgba(255, 255, 255, 0.8); 42 | --plyr-tooltip-radius: 100px; 43 | 44 | --plyr-font-smoothing: true; 45 | } 46 | 47 | .player-wrapper { 48 | span { 49 | font-size: 0.9rem; 50 | } 51 | button { 52 | width: fit-content; 53 | height: fit-content; 54 | } 55 | button:hover { 56 | box-shadow: none; 57 | } 58 | } 59 | 60 | body { 61 | color: #fff; 62 | background: rgb(17, 17, 34); 63 | min-width: 1100px; 64 | } 65 | 66 | a { 67 | all: unset; 68 | cursor: pointer; 69 | } 70 | 71 | ::selection { 72 | background: rgba(rgb(116, 6, 206), 0.7); 73 | } 74 | 75 | h2 { 76 | color: #fff; 77 | font-size: 1.75rem; 78 | } 79 | 80 | .flex-center { 81 | display: flex; 82 | align-items: center; 83 | justify-content: center; 84 | } 85 | .flex-center-col { 86 | display: flex; 87 | flex-flow: column; 88 | align-items: center; 89 | justify-content: center; 90 | } 91 | 92 | button, 93 | .button { 94 | display: flex; 95 | align-items: center; 96 | justify-content: center; 97 | width: 220px; 98 | height: 55px; 99 | font-size: 1.2rem; 100 | font-weight: 600; 101 | @include gradient; 102 | border: none; 103 | border-radius: 300px; 104 | color: #fff; 105 | cursor: pointer; 106 | padding-bottom: 2px; 107 | transition: $trans-med; 108 | span { 109 | margin-right: 0.3rem; 110 | font-size: 1.6rem; 111 | } 112 | } 113 | button:hover, 114 | .button:hover { 115 | box-shadow: 0 0.4rem 1.5rem rgb(103, 14, 204), 116 | 0 0.5rem 2rem rgba(204, 14, 195, 0.4); 117 | 118 | box-shadow: rgba(208, 46, 240, 0.4) 2px 3px, rgba(195, 46, 240, 0.3) 4px 6px, 119 | rgba(169, 46, 240, 0.2) 6px 9px, rgba(208, 46, 240, 0.1) 8px 12px; 120 | } 121 | button:active, 122 | .button:active { 123 | box-shadow: none; 124 | } 125 | 126 | .button-outline { 127 | background: none; 128 | border: 1px solid #fff; 129 | transition: $trans-med; 130 | } 131 | .button-outline:hover { 132 | background: rgba(#fff, 0.05); 133 | } 134 | .button-outline:active { 135 | background: none; 136 | box-shadow: none; 137 | } 138 | 139 | .input-wrapper { 140 | margin: 0.5rem 0; 141 | display: flex; 142 | flex-flow: column; 143 | } 144 | 145 | label { 146 | font-size: 0.8rem; 147 | color: rgba(#fff, 0.7); 148 | } 149 | input { 150 | width: 240px; 151 | font-size: 1rem; 152 | color: #fff; 153 | padding: 0.75rem 1.1rem; 154 | margin-top: 0.4rem; 155 | border: none; 156 | outline: none; 157 | border-radius: 0.5rem; 158 | border: 1px solid rgba(#fff, 0.1); 159 | background: rgba(#fff, 0.05); 160 | box-shadow: 0 0.5rem 1rem rgba(#000, 0.1); 161 | } 162 | input::placeholder { 163 | color: rgba(#fff, 0.2); 164 | } 165 | -------------------------------------------------------------------------------- /assets/icons/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emanuelchristo/movie-night/06544427fba66e95b5fa5492d810ce442c00b281/assets/icons/file.png -------------------------------------------------------------------------------- /assets/icons/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emanuelchristo/movie-night/06544427fba66e95b5fa5492d810ce442c00b281/assets/icons/link.png -------------------------------------------------------------------------------- /assets/icons/youtube.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emanuelchristo/movie-night/06544427fba66e95b5fa5492d810ce442c00b281/assets/images/background.jpg -------------------------------------------------------------------------------- /assets/images/background_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emanuelchristo/movie-night/06544427fba66e95b5fa5492d810ce442c00b281/assets/images/background_1.jpg -------------------------------------------------------------------------------- /assets/images/background_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emanuelchristo/movie-night/06544427fba66e95b5fa5492d810ce442c00b281/assets/images/background_2.jpg -------------------------------------------------------------------------------- /assets/images/background_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emanuelchristo/movie-night/06544427fba66e95b5fa5492d810ce442c00b281/assets/images/background_3.jpg -------------------------------------------------------------------------------- /components/AudioBar.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 28 | -------------------------------------------------------------------------------- /components/Avatar.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | 45 | -------------------------------------------------------------------------------- /components/Controls.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 84 | 85 | 227 | -------------------------------------------------------------------------------- /components/Info.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 54 | 55 | 167 | -------------------------------------------------------------------------------- /components/JoinRoom.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 63 | 64 | 168 | -------------------------------------------------------------------------------- /components/Modal.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | 16 | 71 | -------------------------------------------------------------------------------- /components/Participant.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 85 | 86 | 239 | -------------------------------------------------------------------------------- /components/Player.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 170 | 171 | 240 | -------------------------------------------------------------------------------- /components/RoomSetup.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 133 | 134 | 227 | -------------------------------------------------------------------------------- /components/Spinner.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 14 | 69 | -------------------------------------------------------------------------------- /components/VideoStopped.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 74 | 75 | 139 | -------------------------------------------------------------------------------- /components/modals/EditRoom.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 57 | 58 | 95 | -------------------------------------------------------------------------------- /components/modals/Invite.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 43 | 44 | 82 | -------------------------------------------------------------------------------- /components/modals/VideoLinkModal.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 48 | 49 | 90 | -------------------------------------------------------------------------------- /layouts/landing.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 44 | 45 | 175 | -------------------------------------------------------------------------------- /middleware/README.md: -------------------------------------------------------------------------------- 1 | # MIDDLEWARE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your application middleware. 6 | Middleware let you define custom functions that can be run before rendering either a page or a group of pages. 7 | 8 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware). 9 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // Global page headers 3 | head: { 4 | title: "Movie Night", 5 | htmlAttrs: { 6 | lang: "en" 7 | }, 8 | meta: [ 9 | { charset: "utf-8" }, 10 | { name: "viewport", content: "width=device-width, initial-scale=1" }, 11 | { hid: "description", name: "description", content: "" } 12 | ], 13 | link: [ 14 | { rel: "icon", type: "image/x-icon", href: "/favicon.ico" }, 15 | { rel: "preconnect", href: "https://fonts.gstatic.com" }, 16 | { 17 | rel: "stylesheet", 18 | href: 19 | "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" 20 | } 21 | ] 22 | }, 23 | 24 | // Global CSS 25 | css: [ 26 | "~/assets/css/global.scss", 27 | "@mdi/font/css/materialdesignicons.min.css" 28 | ], 29 | 30 | env: { 31 | SERVER_URL: process.env.SERVER_URL || "http://localhost:5300" 32 | }, 33 | 34 | // Plugins to run before rendering page 35 | plugins: [ 36 | { src: "~/plugins/vue-plyr", mode: "client" }, 37 | { src: "~/plugins/srt-webvtt", mode: "client" }, 38 | { src: "~/plugins/socket-io", mode: "client" }, 39 | { src: "~/plugins/ping", mode: "client" } 40 | ], 41 | 42 | components: true, 43 | 44 | modules: ["@nuxtjs/axios", "nuxt-clipboard"], 45 | 46 | axios: { 47 | // baseURL: "https://movie-night-cris.herokuapp.com/" // Used as fallback if no runtime config is provided 48 | }, 49 | 50 | // Build Configuration 51 | build: {}, 52 | server: { 53 | host: "0.0.0.0" 54 | }, 55 | generate: { 56 | fallback: true 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "movie-night", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxt", 7 | "build": "nuxt build", 8 | "start": "nuxt start", 9 | "generate": "nuxt generate" 10 | }, 11 | "dependencies": { 12 | "@mdi/font": "^5.9.55", 13 | "@nuxtjs/axios": "^5.13.6", 14 | "core-js": "^3.18.0", 15 | "nuxt": "^2.15.8", 16 | "nuxt-clipboard": "0.0.4", 17 | "socket.io-client": "^4.2.0", 18 | "vue-plyr": "^7.0.0" 19 | }, 20 | "devDependencies": { 21 | "fibers": "^5.0.0", 22 | "sass": "^1.42.1", 23 | "sass-loader": "10" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pages/_room.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 113 | 114 | 231 | -------------------------------------------------------------------------------- /pages/createroom.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 86 | 87 | 137 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 35 | 36 | 60 | -------------------------------------------------------------------------------- /pages/joinroom.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 42 | 43 | 93 | -------------------------------------------------------------------------------- /plugins/ping.js: -------------------------------------------------------------------------------- 1 | setInterval(() => { 2 | console.log("ping"); 3 | fetch(process.env.SERVER_URL + "/ping").catch(err => { 4 | console.log("ping failed"); 5 | console.error(err); 6 | }); 7 | }, 3000); 8 | -------------------------------------------------------------------------------- /plugins/socket-io.js: -------------------------------------------------------------------------------- 1 | import { io } from "socket.io-client"; 2 | const socket = io(process.env.SERVER_URL); 3 | 4 | export default ({ app }, inject) => { 5 | inject("socket", socket); 6 | }; 7 | -------------------------------------------------------------------------------- /plugins/srt-webvtt.js: -------------------------------------------------------------------------------- 1 | class WebVTTConverter { 2 | constructor(resource) { 3 | this.resource = resource; 4 | } 5 | 6 | blobToBuffer() { 7 | return new Promise((resolve, reject) => { 8 | const reader = new FileReader(); 9 | reader.addEventListener("loadend", event => { 10 | const buf = event.target.result; 11 | resolve(new Uint8Array(buf)); 12 | }); 13 | reader.addEventListener("error", () => 14 | reject("Error while reading the Blob object") 15 | ); 16 | reader.readAsArrayBuffer(this.resource); 17 | }); 18 | } 19 | /** 20 | * @param {*} blob 21 | * @param {*} success 22 | * @param {*} fail 23 | */ 24 | static blobToString(blob, success, fail) { 25 | const reader = new FileReader(); 26 | reader.addEventListener("loadend", event => { 27 | const text = event.target.result; 28 | success(text); 29 | }); 30 | reader.addEventListener("error", () => fail()); 31 | reader.readAsText(blob); 32 | } 33 | /** 34 | * @param {*} utf8str 35 | */ 36 | static toVTT(utf8str) { 37 | return utf8str 38 | .replace(/\{\\([ibu])\}/g, "") 39 | .replace(/\{\\([ibu])1\}/g, "<$1>") 40 | .replace(/\{([ibu])\}/g, "<$1>") 41 | .replace(/\{\/([ibu])\}/g, "") 42 | .replace(/(\d\d:\d\d:\d\d),(\d\d\d)/g, "$1.$2") 43 | .concat("\r\n\r\n"); 44 | } 45 | /** 46 | * @param {*} str 47 | */ 48 | static toTypedArray(str) { 49 | const result = []; 50 | str.split("").forEach(each => { 51 | result.push(parseInt(each.charCodeAt(), 16)); 52 | }); 53 | return Uint8Array.from(result); 54 | } 55 | 56 | getURL() { 57 | return new Promise((resolve, reject) => { 58 | if (!(this.resource instanceof Blob)) 59 | return reject( 60 | "Expecting resource to be a Blob but something else found." 61 | ); 62 | if (!FileReader) return reject("No FileReader constructor found"); 63 | if (!TextDecoder) return reject("No TextDecoder constructor found"); 64 | return WebVTTConverter.blobToString( 65 | this.resource, 66 | decoded => { 67 | const vttString = "WEBVTT FILE\r\n\r\n"; 68 | const text = vttString.concat(WebVTTConverter.toVTT(decoded)); 69 | const blob = new Blob([text], { type: "text/vtt" }); 70 | this.objectURL = URL.createObjectURL(blob); 71 | return resolve(this.objectURL); 72 | }, 73 | () => { 74 | this.blobToBuffer().then(buffer => { 75 | const utf8str = new TextDecoder("utf-8").decode(buffer); 76 | const vttString = "WEBVTT FILE\r\n\r\n"; 77 | const text = vttString.concat(WebVTTConverter.toVTT(utf8str)); 78 | const blob = new Blob([text], { type: "text/vtt" }); 79 | this.objectURL = URL.createObjectURL(blob); 80 | return resolve(this.objectURL); 81 | }); 82 | } 83 | ); 84 | }); 85 | } 86 | 87 | release() { 88 | URL.createObjectURL(this.objectURL); 89 | } 90 | } 91 | 92 | export default ({ app }, inject) => { 93 | inject("VTTConverter", WebVTTConverter); 94 | }; 95 | -------------------------------------------------------------------------------- /plugins/vue-plyr.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VuePlyr from "vue-plyr/dist/vue-plyr.ssr.js"; 3 | import "vue-plyr/dist/vue-plyr.css"; 4 | 5 | // The second argument is optional and sets the default config values for every player. 6 | Vue.use(VuePlyr, { 7 | plyr: {} 8 | }); 9 | -------------------------------------------------------------------------------- /screenshots/screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emanuelchristo/movie-night/06544427fba66e95b5fa5492d810ce442c00b281/screenshots/screenshot_1.png -------------------------------------------------------------------------------- /screenshots/screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emanuelchristo/movie-night/06544427fba66e95b5fa5492d810ce442c00b281/screenshots/screenshot_2.png -------------------------------------------------------------------------------- /screenshots/screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emanuelchristo/movie-night/06544427fba66e95b5fa5492d810ce442c00b281/screenshots/screenshot_3.png -------------------------------------------------------------------------------- /screenshots/screenshot_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emanuelchristo/movie-night/06544427fba66e95b5fa5492d810ce442c00b281/screenshots/screenshot_4.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emanuelchristo/movie-night/06544427fba66e95b5fa5492d810ce442c00b281/static/favicon.ico -------------------------------------------------------------------------------- /static/file_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emanuelchristo/movie-night/06544427fba66e95b5fa5492d810ce442c00b281/static/file_icon.png -------------------------------------------------------------------------------- /static/sample.vtt: -------------------------------------------------------------------------------- 1 | WEBVTT 2 | 3 | 00:00:00.500 --> 00:00:02.000 4 | The Web is always changing 5 | 6 | 00:00:02.500 --> 00:00:04.300 7 | and the way we access it is changing -------------------------------------------------------------------------------- /static/video_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emanuelchristo/movie-night/06544427fba66e95b5fa5492d810ce442c00b281/static/video_icon.png -------------------------------------------------------------------------------- /store/index.js: -------------------------------------------------------------------------------- 1 | const roomDefault = { 2 | id: "abc-defg-hij", 3 | name: null, 4 | imageLink: null, 5 | started: false, 6 | joined: false, 7 | playing: false, 8 | 9 | video: { 10 | added: false, 11 | type: null, 12 | link: null, 13 | thumbnail: null, 14 | title: null 15 | }, 16 | 17 | player: { 18 | loaded: "bool", 19 | time: "number", 20 | playing: "bool", 21 | volume: "number", 22 | speed: "string", 23 | captions: "link string" 24 | }, 25 | 26 | host: { 27 | id: "abcd", 28 | nickName: "Cris", 29 | ready: true, 30 | micOff: false, 31 | volume: 0 32 | }, 33 | participants: [ 34 | { 35 | id: "jkji", 36 | nickName: "Jake", 37 | ready: false, 38 | micOff: true, 39 | volume: 0 40 | }, 41 | { 42 | id: "jkjsfasdi", 43 | nickName: "Thor", 44 | ready: true, 45 | micOff: false, 46 | volume: 0.5 47 | } 48 | ], 49 | voiceChat: { 50 | micOff: false, 51 | soundOff: true 52 | }, 53 | chat: [ 54 | { 55 | id: null, 56 | userId: null, 57 | nickName: null, 58 | timestamp: null, 59 | message: "string" 60 | } 61 | ] 62 | }; 63 | 64 | export const state = () => ({ 65 | loggedIn: false, 66 | userId: null, 67 | nickName: null, 68 | isHost: false, 69 | room: roomDefault 70 | }); 71 | 72 | export const actions = { 73 | login({ commit }, nickName) { 74 | return new Promise((resolve, reject) => { 75 | try { 76 | // Checking if userId exists 77 | const userId = window.localStorage.getItem("userId"); 78 | // Loggin in 79 | console.log("login req:", { userId, nickName }); 80 | this.$socket.emit("auth:login", { userId, nickName }, res => { 81 | if (!res) { 82 | commit("setLoggedIn", { 83 | loggedIn: false, 84 | userId: null, 85 | nickName: null 86 | }); 87 | } else { 88 | // Setting userId 89 | window.localStorage.setItem("userId", res.id); 90 | commit("setLoggedIn", { 91 | loggedIn: true, 92 | userId: res.id, 93 | nickName: res.nickName 94 | }); 95 | } 96 | resolve(); 97 | return; 98 | }); 99 | } catch (err) { 100 | reject(); 101 | } 102 | }); 103 | }, 104 | 105 | logout({ commit }) { 106 | const userId = window.localStorage.getItem("userId"); 107 | window.localStorage.removeItem("userId"); 108 | this.$socket.emit("auth:logout", { userId }, () => { 109 | this.$router.go(); 110 | commit("setLoggedIn", { 111 | loggedIn: false, 112 | userId: null, 113 | nickName: null 114 | }); 115 | }); 116 | }, 117 | 118 | createRoom({ commit, state }) { 119 | return new Promise((resolve, reject) => { 120 | console.log("createRoom req:", { userId: state.userId }); 121 | this.$socket.emit("room:create", state.userId, room => { 122 | console.log("createRoom res:", { room }); 123 | commit("setRoom", room); 124 | resolve(room.id); 125 | }); 126 | }); 127 | }, 128 | 129 | async fetchRoom({ commit }, roomId) { 130 | console.log("fetchRoom req:", { roomId }); 131 | this.$socket.emit("room:get", roomId, room => { 132 | console.log("fetchRoom res:", { room }); 133 | commit("setRoom", room); 134 | }); 135 | // Subscribing to room changes 136 | this.$socket.on("room:updated", room => { 137 | console.log("roomUpdated: ", { room }); 138 | commit("setRoom", room); 139 | }); 140 | }, 141 | 142 | async joinRoom({ commit, state, dispatch }, data) { 143 | console.log("Join Room Called"); 144 | dispatch("login", data).then(() => { 145 | console.log("joinRoom req:", { 146 | roomId: state.room.id, 147 | userId: state.userId 148 | }); 149 | this.$socket.emit("room:join", { 150 | roomId: state.room.id, 151 | userId: state.userId 152 | }); 153 | }); 154 | }, 155 | 156 | addVideo({ commit, state }, data) { 157 | return new Promise(async (resolve, reject) => { 158 | console.log("addVideo"); 159 | try { 160 | let video = { 161 | added: true, 162 | type: null, 163 | link: null, 164 | thumbnail: null, 165 | title: null 166 | }; 167 | if (data.type == "youtube") { 168 | // YouTube video metadata server url 169 | const url = "https://noembed.com/embed?url=" + data.link; 170 | // YouTube metadata 171 | const res = await this.$axios.get(url); 172 | video = { 173 | added: true, 174 | type: "youtube", 175 | link: data.link, 176 | thumbnail: res.data.thumbnail_url, 177 | title: res.data.title 178 | }; 179 | } else if (data.type == "file") { 180 | video.type = "file"; 181 | video.title = data.fileName; 182 | video.link = null; 183 | video.thumbnail = "/file_icon.png"; 184 | } else if (data.type == "video") { 185 | video.type = "video"; 186 | video.link = data.link; 187 | video.thumbnail = "/video_icon.png"; 188 | video.title = "Video from link"; 189 | } 190 | 191 | // Making request to server 192 | if (state.isHost) { 193 | this.$socket.emit("room:addvideo", { 194 | userId: state.userId, 195 | roomId: state.room.id, 196 | video: video 197 | }); 198 | } 199 | if (data.type == "file") { 200 | commit("setVideoFileName", data.fileName); 201 | commit("setVideoLink", data.fileURL); 202 | } 203 | resolve(); 204 | } catch (err) { 205 | console.error(err); 206 | reject(); 207 | } 208 | }); 209 | }, 210 | 211 | async removeVideo({ commit, state }) { 212 | this.$socket.emit("room:removevideo", { 213 | userId: state.userId, 214 | roomId: state.room.id, 215 | video: {} 216 | }); 217 | }, 218 | 219 | async startRoom({ commit, state }, data) { 220 | this.$socket.emit("room:startroom", { 221 | userId: state.userId, 222 | roomId: state.room.id, 223 | data 224 | }); 225 | }, 226 | 227 | async setRoomInfo({ commit, state }, data) { 228 | this.$socket.emit("room:setroominfo", { 229 | userId: state.userId, 230 | roomId: state.room.id, 231 | data 232 | }); 233 | }, 234 | 235 | async stop({ commit, state }) { 236 | this.$socket.emit("room:stop", { 237 | userId: state.userId, 238 | roomId: state.room.id 239 | }); 240 | }, 241 | 242 | async sync({ commit, state }, data) { 243 | this.$socket.emit("room:sync", { 244 | userId: state.userId, 245 | roomId: state.room.id, 246 | data 247 | }); 248 | }, 249 | 250 | async leaveRoom({ commit, state }) { 251 | this.$socket.emit("room:leave", { 252 | userId: state.userId, 253 | roomId: state.room.id 254 | }); 255 | } 256 | }; 257 | 258 | export const mutations = { 259 | setLoggedIn(state, data) { 260 | state.loggedIn = data.loggedIn; 261 | state.userId = data.userId; 262 | state.nickName = data.nickName; 263 | }, 264 | setVideoLink(state, data) { 265 | state.room.video.link = data; 266 | }, 267 | setVideoFileName(state, data) { 268 | state.room.video.title = data; 269 | }, 270 | setCreateRoom(state, data) { 271 | state.room = data; 272 | state.room.id = data._id; 273 | delete state.room._id; 274 | state.room.joined = true; 275 | state.isHost = true; 276 | }, 277 | setRoom(state, data) { 278 | if (!data) { 279 | state.room = roomDefault; 280 | return; 281 | } 282 | const videoLink = state.room.video.link; 283 | const fileName = state.room.video.title; 284 | state.room = data; 285 | state.room.id = data._id || data.id; 286 | delete state.room._id; 287 | if (state.room.host.id == state.userId) { 288 | state.room.joined = true; 289 | state.isHost = true; 290 | } else { 291 | const userInParticipants = checkParticipant( 292 | state.room.participants, 293 | state.userId 294 | ); 295 | if (userInParticipants) state.room.joined = true; 296 | } 297 | if (state.room.video.type == "file") { 298 | state.room.video.link = videoLink; 299 | state.room.video.title = fileName; 300 | } 301 | } 302 | }; 303 | 304 | function checkParticipant(participants, userId) { 305 | return participants.find(p => p.id == userId); 306 | } 307 | --------------------------------------------------------------------------------