TogetherTube is a YouTube client that brings YouTube and playback sync together!
💻Live demo | 💡 The Concept | 📝Features | 📺 YouTube Player API | 📖 YouTube Data API | 🔄 Data lifecycle diagram | 🤖 Installation | 🤝 Sources | 📝 License |
TogetherTube is a YouTube client that brings YouTube and playback sync together using Socket.io. Users can enter a YouTube URL in the search bar to open a player. On that page, they'll be greeted with the desired video along a live chat and related videos.
Concept 1
The first concept was similar to the final concept. Unlike the final version, it used an oauth login screen that would allow personalized rooms. The main difference was to control your other devices/tabs. After a review, it became clear that it should focus on the multi-user aspect rather than single-user&multi-device.The (final) concept
While processing the feedback on my first concept, I decided to follow the multi-user comment. In this iteration I focussed on syncing playback and adding more multi-user features (the chat) and applied them in the concept. To improve and stimulate usage and UX, I stripped away the login and personalization such as playlists and custom rooms. Here's how the data will flow within the application.- Search any YouTube video
- Play any YouTube video
- Watch any YouTube video together
- Chat together on any YouTube video
- Get related videos on any YouTube video
- Save TogetherTube as PWA on your device
Must haves
Core functionality
- Create room by YouTube link
- Count of members in a room
Player
- Play video in the room
Chat
- Chat with other viewers
- Cache messages and restrict chat history to one hour
Related videos
- Display related videos
- Related video creates and joins new room
- Cache related videos on an external DB
Should haves
Core functionality
- Mobile friendly layout
- A search bar to paste links and open new rooms
Player
- Sync position in the video with the room
- Sync the playback state with the room
Chat
- Know when others join the room
- Have a name when chatting if not anonymous
Could haves
Core functionality
- Custom playbar
- Video title as tab title
- Display current active rooms
- Technical restrictions; find a way to monitor room sizes without using window.onbeforeunload and applying changes
- Creating playlists starting from current video
Won't have this time
- User roles for playback controls
- Manually searching videos
- Redirect to next video in related videos section
Source: link to iframe API
The YouTube player API provides an iFrame wherein the video player is found. The API supports two different approaches; automatically where the iFrame player embeds a video link, then renders it in the iFrame:
<iframe id="ytplayer" type="text/html" width="640" height="360"
src="YouTubeLinkGoesHere?autoplay=1&origin=http://example.com/"
frameborder="0"></iframe>
In this case, the player takes the given url source along with its parameters ?autoplay=1&origin=http://example.com/"
to generate a video player within the iframe. The parameters are optional and can be expended with additional parameters found in the documentation: link to additional parameters.
The manual approach, however, is different. This approach needs an additional piece of javascript to render the iframe. The main advantages are:
- Programmatically change sources
- Handle player events manually
- Retrieve additional meta data
For this project, we're using the manual way to handle data
events:
- state:
1
- video is playing - state:
2
- video is paused - state:
3
- video is seeking
Knowing this, we can use javascript to interact with the Player
object, which contains methods such as pauseVideo()
, playVideo()
, seekTo()
, and other interactions normally found in the UI. See complete list
Source: link to YouTube Data API
This project uses the YouTube Data API to retrieve and render related videos to the video that is currently playing. To achieve that, the Search
endpoint is used: link to the Search endpoint. This endpoint takes a video id and returns videos related to it. The data returned contains the following from which only the snippets are used:
{
"kind": "youtube#searchResult",
"etag": etag,
"id": {
"kind": string,
"videoId": string,
"channelId": string,
"playlistId": string
},
"snippet": {
"publishedAt": datetime,
"channelId": string,
"title": string,
"description": string,
"thumbnails": {
(key): {
"url": string,
"width": unsigned integer,
"height": unsigned integer
}
},
"channelTitle": string,
"liveBroadcastContent": string
}
}
- State
Thestate
event handles the seeking events sent from the client and broadcasts it to other clients. The state contains theroomid
,timestamp
, andplaystate
{
id: String,
playing: Boolean,
timestamp: Number,
room: String
}
- Playback
Theplayback
event handles the playback state of a give room. The array contains a room name and its playback state that is emitted to other clients connected to that room. The playback state is a boolean that plays the video on true and pauses it on false.
{
room: String,
state: Boolean
}
- Message
Themessage
event handles the sent messages from clients. The client emits amessage
event which is fetched by the server, the server caches the message using thecacheMessage
function, and then emits the received message object to the entire room which is then rendered to in the DOM.
{
"roomName": [
{
"name": String,
"message": String,
"timestamp": Number,
"room": String
}
]
}
This projects optimizes data storage and tries to serve them as efficiently as possible. The two features that handle data, the related videos and messages, are stored and managed differently.
Related cache
It is created when the user opens a room that has not been cached previously. The cache has the following structure:
{
"roomId": [
{
"kind": "youtube#searchResult",
"etag": etag,
"id": {
"kind": string,
"videoId": string,
"channelId": string,
"playlistId": string
},
"snippet": {
"publishedAt": datetime,
"channelId": string,
"title": string,
"description": string,
"thumbnails": {
(key): {
"url": string,
"width": unsigned integer,
"height": unsigned integer
}
},
"channelTitle": string,
"liveBroadcastContent": string
}
}
]
}
While the related videos is a nice-to-have feature, it is not essential to the core functionality. Therefore, it has been decided to load the related cache in async
. Furthermore, in consideration to the API quota limit, the cache is hosted on a remote, cloud-based Redis database. This does add some latency; the application fetches data over the internet, transforms the data, then sends it to the client over the internet.
Messages cache
The messages are cached in a local JSON file called messagesCache
with the following structure:
{
"roomName": [
{
"name": String,
"message": String,
"timestamp": Number,
"room": String
}
]
}
The chat functionality is instant messaging therefore it is essential to fetch, save, and send data as fast as possible. That considered, it has been decided that storing messages in a local file is the preferred method.
Dependencies
- Node
- Express
^4.17.1
- EJS
^3.1.6
- Dotenv
^8.2.0
- Body-parser
^1.19.0
- Socket.io
^4.0.1
- Axios
^0.21.1
- Async-redis
^1.1.7
- Lowdb
^1.0.0
Get a YouTube API key
Requirements:
- Google account
- Redis database
Get your Youtube API key:
- Open the cloud console link
- Create a project
- Open the project
- Go to the
API Console
- Enable
YouTube Data API v3
- Copy your API key to the
.env
file in project root
Set up Redis database:
- Local:
- Windows: guide
- MacOS using
homebrew
:brew install redis
- Unix using buildtools: download binary
- Cloud (preferred method)
- Create an account at RedisLabs
- Copy credentials to the
.env
in project root
Run the project:
- Install dependencies
npm install
- Run project
npm start