Build a Video Downloader Chrome Extension: Complete Tutorial 2025
Video content has become the dominant form of media on the internet. From educational tutorials to entertainment, users frequently want to save videos for offline viewing. Building a video downloader Chrome extension is an excellent project that teaches you fundamental concepts of extension development while creating a genuinely useful tool.
In this comprehensive guide, we will walk through building a fully functional video downloader Chrome extension. You will learn how to detect media files on web pages, interact with the Chrome Downloads API, handle different video formats, and create a user-friendly popup interface. By the end of this tutorial, you will have a complete working extension that can download videos from various websites.
Why Build a Video Downloader Extension?
The demand for video downloader extensions remains consistently high. Users want to save videos for offline viewing during travel, save bandwidth by buffering once, or archive educational content for future reference. Building a media grabber extension teaches you several valuable skills that transfer to other extension projects.
Key Learning Outcomes
When you build this video downloader extension, you will gain hands-on experience with several important Chrome extension concepts. First, you will learn how content scripts interact with web page DOM to detect media elements. Second, you will understand how to communicate between different extension components using message passing. Third, you will work with the Chrome Downloads API to initiate and manage downloads. Fourth, you will implement popup interfaces that provide real-time feedback to users.
These skills form the foundation for many types of extensions beyond video downloading. Tab managers, note-taking extensions, and productivity tools all use similar patterns for interacting with web pages and the Chrome API.
Extension Overview
Our video downloader extension will include several core features. The extension will scan web pages for video elements and media sources automatically. It will display detected videos in a clean popup interface with download options. Users will be able to choose video quality and format before downloading. The extension will handle both embedded videos and direct media URLs.
Project Structure
Every Chrome extension needs a well-organized structure. Let’s set up the project files before writing any code.
Creating the Project Directory
Create a new folder for your extension project. Inside this folder, we will create several essential files. The manifest.json file defines the extension configuration. The popup.html and popup.js files handle the user interface. The content.js file runs on web pages to detect videos. The background.js file manages background processing and downloads.
video-downloader/
├── manifest.json
├── popup.html
├── popup.js
├── popup.css
├── content.js
├── background.js
└── icons/
├── icon16.png
├── icon48.png
└── icon128.png
This structure keeps different parts of your extension organized and maintainable. Each file has a specific responsibility, making it easier to debug and extend later.
Manifest V3 Configuration
The manifest.json file is the heart of every Chrome extension. For our video downloader, we need to declare specific permissions and define the extension’s components.
{
"manifest_version": 3,
"name": "Video Downloader Pro",
"version": "1.0.0",
"description": "Download videos from any website with one click",
"permissions": [
"downloads",
"activeTab",
"scripting"
],
"host_permissions": [
"<all_urls>"
],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_idle"
}
],
"background": {
"service_worker": "background.js",
"type": "module"
},
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}
The manifest declares several important permissions. The “downloads” permission allows us to trigger file downloads. The “activeTab” permission gives us access to the currently active tab when the user clicks the extension icon. The “scripting” permission enables us to execute content scripts on pages.
The host permissions use “
Content Script: Detecting Videos
The content script runs on every web page and is responsible for finding video elements. This is where the core logic of video detection happens.
Writing content.js
// content.js - Runs on web pages to detect video elements
(function() {
// Store detected videos
let detectedVideos = [];
// Function to find all video elements on the page
function findVideos() {
const videos = [];
// Find <video> elements
document.querySelectorAll('video').forEach(video => {
const videoInfo = extractVideoInfo(video);
if (videoInfo.src || videoInfo.sources.length > 0) {
videos.push(videoInfo);
}
});
// Find video elements in iframes (limited due to cross-origin)
document.querySelectorAll('iframe[src*="video"], iframe[src*="player"]').forEach(iframe => {
videos.push({
type: 'iframe',
src: iframe.src,
pageUrl: window.location.href,
pageTitle: document.title
});
});
// Look for media URLs in page source
findMediaUrls();
return videos;
}
// Extract video information from a video element
function extractVideoInfo(videoElement) {
const info = {
type: 'video element',
src: videoElement.src || '',
sources: [],
poster: videoElement.poster || '',
pageUrl: window.location.href,
pageTitle: document.title
};
// Get all source elements
videoElement.querySelectorAll('source').forEach(source => {
if (source.src) {
info.sources.push({
src: source.src,
type: source.type || 'video/mp4'
});
}
});
// Get video attributes
info.width = videoElement.width;
info.height = videoElement.height;
info.duration = videoElement.duration;
return info;
}
// Find media URLs in page scripts and network requests
function findMediaUrls() {
const mediaPatterns = [
/\.mp4($|\?)/i,
/\.webm($|\?)/i,
/\.m3u8($|\?)/i,
/video\.mp4/i,
/media\.googlevideo\.com/i,
/\.(mp4|webm|mov|avi)($|\?)/i
];
// Scan script tags for media URLs
document.querySelectorAll('script').forEach(script => {
if (script.textContent) {
mediaPatterns.forEach(pattern => {
const matches = script.textContent.match(new RegExp(pattern, 'gi'));
if (matches) {
matches.forEach(url => {
// Clean up the URL
const cleanUrl = url.split('"')[0].split("'")[0].split(' ')[0];
if (cleanUrl.startsWith('http')) {
addDetectedVideo({
type: 'media url',
src: cleanUrl,
pageUrl: window.location.href,
pageTitle: document.title
});
}
});
}
});
}
});
}
// Add video to detected list if not duplicate
function addDetectedVideo(video) {
const isDuplicate = detectedVideos.some(v => v.src === video.src);
if (!isDuplicate && video.src) {
detectedVideos.push(video);
// Notify background script
chrome.runtime.sendMessage({
action: 'videoDetected',
video: video
});
}
}
// Listen for messages from popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'scanPage') {
const videos = findVideos();
detectedVideos = videos;
sendResponse({ videos: videos, count: videos.length });
}
return true;
});
// Initial scan after page loads
setTimeout(() => {
findVideos();
}, 2000);
// Re-scan on dynamic content changes
const observer = new MutationObserver(() => {
findVideos();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
})();
This content script performs several important functions. First, it scans the page for native video elements using querySelectorAll. Second, it searches for video iframes that might contain media players. Third, it analyzes script tags to find media URLs that are dynamically loaded. Fourth, it uses a MutationObserver to detect videos that load after the initial page render.
The script communicates with the background script whenever it finds new videos, allowing real-time updates to the user interface.
Background Script: Managing Downloads
The background script handles the actual download process. It receives video information from content scripts and initiates downloads using the Chrome Downloads API.
Writing background.js
// background.js - Manages downloads and coordinates components
// Store pending downloads
const downloadQueue = [];
// Listen for video detection from content scripts
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'videoDetected') {
// Store the detected video
const tabId = sender.tab?.id;
if (tabId) {
chrome.tabs.sendMessage(tabId, {
action: 'updateVideoList',
video: request.video
}).catch(() => {
// Tab might not have content script loaded
});
}
}
if (request.action === 'startDownload') {
startDownload(request.video).then(downloadId => {
sendResponse({ success: true, downloadId: downloadId });
}).catch(error => {
sendResponse({ success: false, error: error.message });
});
return true; // Keep message channel open for async response
}
});
// Download function
async function startDownload(video) {
const filename = generateFilename(video);
const options = {
url: video.src,
filename: filename,
saveAs: true,
conflictAction: 'uniquify'
};
try {
const downloadId = await chrome.downloads.download(options);
return downloadId;
} catch (error) {
console.error('Download failed:', error);
throw error;
}
}
// Generate appropriate filename from video info
function generateFilename(video) {
const timestamp = new Date().toISOString().slice(0, 10);
let filename = 'video';
// Try to extract meaningful name from URL
if (video.pageTitle && video.pageTitle !== 'Untitled') {
// Sanitize the title
filename = video.pageTitle
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '_')
.substring(0, 50);
}
// Add format extension
if (video.src.includes('.mp4') || video.type?.includes('mp4')) {
filename += '.mp4';
} else if (video.src.includes('.webm')) {
filename += '.webm';
} else {
filename += '.mp4';
}
return `VideoDownloader/${filename}`;
}
// Handle download events
chrome.downloads.onCreated.addListener((downloadItem) => {
console.log('Download started:', downloadItem.id);
});
chrome.downloads.onError.addListener((downloadItem) => {
console.error('Download error:', downloadItem.error);
});
chrome.downloads.onCompleted.addListener((downloadItem) => {
console.log('Download completed:', downloadItem.id);
// Show notification
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon128.png',
title: 'Download Complete',
message: `Downloaded: ${downloadItem.filename}`
});
});
The background script acts as a coordinator between the content script and the Chrome Downloads API. It handles incoming video detections, processes download requests, and manages file naming. It also provides user feedback through notifications when downloads complete.
Popup Interface
The popup provides the user interface for your extension. This is what users see when they click the extension icon in Chrome.
popup.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Video Downloader</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="container">
<header>
<h1>Video Downloader</h1>
<p class="subtitle">Download videos from any page</p>
</header>
<div class="scan-status" id="scanStatus">
<button id="scanBtn" class="primary-btn">Scan for Videos</button>
</div>
<div class="video-list" id="videoList">
<p class="empty-state">Click "Scan for Videos" to find media on this page.</p>
</div>
<div class="footer">
<p class="info">Videos are detected automatically when you visit a page.</p>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
popup.css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
width: 350px;
min-height: 300px;
background: #f5f5f5;
color: #333;
}
.container {
padding: 16px;
}
header {
text-align: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e0e0e0;
}
h1 {
font-size: 18px;
font-weight: 600;
color: #1a73e8;
}
.subtitle {
font-size: 12px;
color: #666;
margin-top: 4px;
}
.scan-status {
margin-bottom: 16px;
}
.primary-btn {
width: 100%;
padding: 10px 16px;
background: #1a73e8;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.primary-btn:hover {
background: #1557b0;
}
.primary-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.video-list {
max-height: 400px;
overflow-y: auto;
}
.video-item {
background: white;
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.video-item h3 {
font-size: 13px;
margin-bottom: 6px;
word-break: break-word;
}
.video-meta {
font-size: 11px;
color: #666;
margin-bottom: 8px;
}
.download-btn {
width: 100%;
padding: 8px;
background: #34a853;
color: white;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: background 0.2s;
}
.download-btn:hover {
background: #2d8e47;
}
.empty-state {
text-align: center;
color: #666;
font-size: 13px;
padding: 20px;
}
.footer {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e0e0e0;
}
.info {
font-size: 11px;
color: #888;
text-align: center;
}
.loading {
text-align: center;
padding: 20px;
color: #666;
}
.error {
color: #d93025;
text-align: center;
padding: 10px;
background: #fce8e6;
border-radius: 4px;
font-size: 12px;
}
popup.js
// popup.js - Handles popup interactions
document.addEventListener('DOMContentLoaded', () => {
const scanBtn = document.getElementById('scanBtn');
const videoList = document.getElementById('videoList');
let videos = [];
// Scan button click handler
scanBtn.addEventListener('click', async () => {
scanBtn.disabled = true;
scanBtn.textContent = 'Scanning...';
videoList.innerHTML = '<div class="loading">Scanning page for videos...</div>';
try {
// Get current tab
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab) {
throw new Error('No active tab found');
}
// Send message to content script to scan for videos
const response = await chrome.tabs.sendMessage(tab.id, { action: 'scanPage' });
if (response && response.videos) {
videos = response.videos;
displayVideos(videos);
} else {
videoList.innerHTML = '<p class="empty-state">No videos found on this page.</p>';
}
} catch (error) {
console.error('Scan error:', error);
videoList.innerHTML = `<p class="error">Could not scan page. Make sure you are on a web page and try again.</p>`;
} finally {
scanBtn.disabled = false;
scanBtn.textContent = 'Scan for Videos';
}
});
// Display videos in the popup
function displayVideos(videoList_) {
if (!videoList_ || videoList_.length === 0) {
videoList.innerHTML = '<p class="empty-state">No videos found on this page.</p>';
return;
}
videoList.innerHTML = videoList_.map((video, index) => `
<div class="video-item">
<h3>Video ${index + 1}</h3>
<p class="video-meta">${video.src.substring(0, 60)}...</p>
<button class="download-btn" data-index="${index}">Download</button>
</div>
`).join('');
// Add click handlers to download buttons
document.querySelectorAll('.download-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
const index = parseInt(e.target.dataset.index);
const video = videoList_[index];
await downloadVideo(video);
});
});
}
// Download video function
async function downloadVideo(video) {
try {
// Send download request to background script
const response = await chrome.runtime.sendMessage({
action: 'startDownload',
video: video
});
if (response && response.success) {
alert('Download started! Check your downloads folder.');
} else {
alert('Download failed: ' + (response?.error || 'Unknown error'));
}
} catch (error) {
console.error('Download error:', error);
alert('Download failed: ' + error.message);
}
}
// Listen for video updates from background
chrome.runtime.onMessage.addListener((request) => {
if (request.action === 'updateVideoList') {
videos.push(request.video);
displayVideos(videos);
}
});
});
Testing Your Extension
Now that you have created all the necessary files, it’s time to test your extension in Chrome.
Loading the Extension
Open Chrome and navigate to chrome://extensions/. Enable “Developer mode” using the toggle in the top right corner. Click “Load unpacked” and select your extension’s folder. Your extension should now appear in the extension list and the popup should open when you click the icon.
Testing the Video Downloader
Visit a website with videos, such as a news site or video hosting platform. Click your extension icon and then click “Scan for Videos.” The extension should detect video elements on the page and display them in the list. Click “Download” on any video to start the download process.
Debugging Common Issues
If videos are not being detected, check the console for errors. Make sure the content script is loading correctly. Some websites use advanced techniques to embed videos that may require additional detection logic.
For download issues, verify that the extension has the necessary permissions in the manifest. The Downloads API requires specific permissions to function properly.
Advanced Features to Consider
Once you have the basic video downloader working, consider adding these advanced features to make your extension more powerful.
Quality Selection
Implement a feature that detects multiple quality versions of the same video. Allow users to choose between different resolutions and file sizes before downloading.
Batch Downloads
Add functionality to download multiple videos at once. This is particularly useful for downloading entire video playlists.
Format Conversion
Integrate a library to convert videos between different formats. This allows users to save videos in formats compatible with their devices.
Progress Tracking
Implement a download progress indicator in the popup. This helps users monitor large file downloads.
URL Input
Allow users to paste video URLs directly into the extension for downloading videos not currently open in the browser.
Best Practices and Considerations
When building and distributing a video downloader extension, keep these important considerations in mind.
Legal and Ethical Considerations
Respect copyright laws and website terms of service. Only download videos that you have the legal right to download. Many content creators rely on video views for income, and downloading videos may violate terms of service for some platforms.
Performance Optimization
Minimize the impact on page load times by running your content script at the appropriate time. Use efficient selectors and avoid unnecessary DOM manipulation.
User Privacy
Be transparent about what data your extension collects and how it uses it. Avoid collecting unnecessary user information.
Error Handling
Implement robust error handling throughout your extension. Users should receive clear feedback when something goes wrong.
Conclusion
Building a video downloader Chrome extension is an excellent project that teaches you fundamental extension development concepts. You have learned how to detect media on web pages, communicate between extension components, manage downloads, and create user interfaces.
The skills you gained in this tutorial transfer directly to many other extension projects. Whether you want to build productivity tools, developer utilities, or creative applications, understanding content scripts, background scripts, and the Chrome API is essential.
Remember to test thoroughly across different websites and browsers. Video detection can be challenging due to the variety of ways websites embed media. Continue refining your detection logic to support more use cases.
Start building your extension today and join the thousands of developers creating useful tools for Chrome users worldwide.