Trying to create a chrome extension (manifest v.3) that can record screen. The flow is:
Showing a record button in popup
Clicking a record button opens an html page in background
html page sends request to background to show desktop selector for the active tab
User selects the window/tab and start recording
Here is the code (https://stackblitz.com/edit/web-platform-mxfsyx?file=index.html):
Created a record button in popup
document.querySelector('#startFromBackgroundPage')
.addEventListener('click', function(event) {
chrome.runtime.sendMessage(
{ event: 'open-bg-page' }, function(response) { console.log(response); });
});
Background page
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if (message.event === 'open-bg-page') {
chrome.tabs.create({
url: 'index.html',
selected: false,
});
}
if (message.event === 'record') {
chrome.tabs.query({ currentWindow: true, active: true }, function (tabs) {
chrome.desktopCapture.chooseDesktopMedia(
["screen", "window"],
tabs[0],
function(id, options) {
chrome.runtime.sendMessage(
{'mediaId': id, options: options});
});
});
}
});
index.html/app.js
// Send message as soon page is loaded.
setTimeout(() => {
chrome.runtime.sendMessage(
{ event: 'record' }, function(response) { console.log(response); });
}, 500);
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
console.log('on message', message);
if (message.mediaId) {
setTimeout(() => {
onAccessApproved(message.mediaId, message.options);
}, 100);
}
});
// Launch webkitGetUserMedia() based on selected media id.
function onAccessApproved(id, options) {
if (!id) {
console.log('Access rejected.');
return;
}
var audioConstraint = {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: id
}
};
if (!options.canRequestAudioTrack)
audioConstraint = false;
navigator.getUserMedia({
audio: audioConstraint,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: id,
maxWidth:screen.width,
maxHeight:screen.height} }
}, gotStream, getUserMediaError);
}
function getUserMediaError(error) {
document.querySelector('#message').innerHTML = error;
console.log('navigator.getUserMedia() error: ', error);
}
But its throwing error: NotAllowedError: Invalid state
It works fine If I replace tabs[0] with sender.tab in background.js. But then it shows the desktop/window chooser on the background page which I don't want. I want to record the current active tab. I am handling it in background page so even the page is refresh/closed the video recording continues.
Related
I am developing my first chrome extension, I want to have a toggle button on my popup which allows users to enable or disable the extension for that site, though not completely disable the extension but technically control the execution of the content.js for that particular domain.
Something similar to
popup.js
const toggleBtn = document.querySelector(".toggle");
toggleBtn.addEventListener("click", function(){
if(toggleBtn.checked === true){
console.log('inject content.js');
}else{
console.log('remove content.js');
}
});
I can see there is a way to inject it but there is no way to remove it.
chrome.scripting.executeScript({
target: {tabId},
files: ['content.js'],
});
Note, I don't want to reload the page but want to disable the content.js. How can I achieve this functionality inside my chrome extension. I see there are many extensions doing this like Adblocker and Wordtune.
I went through few SO threads like this one but most of them are for Manifest V2 and few packages that are recommended are obsolate now. I also could not find a complete example anywhere.
Todo
I should be able to turn the extension on/off for the current domain from within the Popup.
It should store the state inside a chrome.storage.sync so that next time I installed the extension I should not again turn on/off for the selected websites.
As soon as I toggle the button, the changes should be visible if I have opened the same domain in another tab.
The extension's icon should be changed in real time as well e.g grey icon for disabled status
If I close and open a website, the icon and the toggle button should be restored their on/off status and update the icon accordingly.
My Attempt (Solves #toDo 1,2,3,4,5)
background.js
const icons = {
enabled: {
'16': 'icon-16.png',
'19': 'icon-19.png',
'38': 'icon-38.png',
'48': 'icon-48.png',
'128': 'icon-128.png'
},
disabled: {
'16': 'icon-16-off.png',
'19': 'icon-19-off.png',
'38': 'icon-38-off.png',
'48': 'icon-48-off.png',
'128': 'icon-128-off.png'
}
};
const getExtensionStatus = host => new Promise((resolve, reject) => {
chrome.storage.sync.get(host, result => {
if (result[host] !== undefined) {
resolve(result[host]);
} else {
setExtensionStatus(host, true);
}
});
});
const setExtensionStatus = (host, toggleBtnStatus) => {
const data = { [host]: toggleBtnStatus };
chrome.storage.sync.set(data, () => {});
};
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === "install") {
chrome.action.setIcon({ path: icons.enabled });
}
});
const init = async tab => {
const url = new URL(tab.url);
if (url.protocol !== "chrome-extension:") {
const host = url.host;
const status = await getExtensionStatus(host);
const icon = status ? icons.enabled : icons.disabled;
chrome.action.setIcon({ path: icon });
}
};
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === "complete") init(tab);
});
chrome.tabs.onActivated.addListener(info => {
chrome.tabs.get(info.tabId, init);
});
chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
if (request.type === "getExtensionStatus") {
const status = await getExtensionStatus(request.host);
sendResponse({ status });
return true;
} else if (request.type === "setExtensionStatus") {
setExtensionStatus(request.host, request.status);
const icon = request.status ? icons.enabled : icons.disabled;
chrome.action.setIcon({ path: icon });
}
});
popup.js
document.addEventListener("DOMContentLoaded", function () {
const toggleBtn = document.querySelector(".toggle");
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const host = new URL(tabs[0].url).host;
chrome.runtime.sendMessage({ type: "getExtensionStatus", host }, (response) => {
toggleBtn.checked = response.status;
});
toggleBtn.addEventListener("click", () => {
chrome.runtime.sendMessage({ type: "setExtensionStatus", host, status: toggleBtn.checked });
});
});
});
What left?
The content.js is not available after some time of inactiveness of the tab. How can I inject the script again and avoid running event multiple times?
content.js
document.addEventListener("keydown", listenEnterkey);
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === "setExtensionStatus") {
console.log(request.status);
if (request.status) {
// Add event listener here
document.addEventListener("keydown", listenEnterkey);
} else {
// Remove event listener here
document.removeEventListener("keydown", listenEnterkey);
}
}
});
For example, if you add a button with a script, I don't think the added button will disappear even if you remove the script.
Therefore, the ON/OFF function is built into the script, and it is operated by the message.
manifest.json
{
"name": "content_scripts + popup",
"version": "1.0",
"manifest_version": 3,
"content_scripts": [
{
"js": [
"matches.js"
],
"matches": [
"<all_urls>"
]
}
],
"host_permissions": [
"<all_urls>"
],
"action": {
"default_popup": "popup.html"
}
}
matches.js
console.log("matches.js");
let isEnabled = true;
console.log(isEnabled);
chrome.runtime.onMessage.addListener((message) => {
switch (message) {
case "on":
isEnabled = true;
break;
default:
isEnabled = false;
break;
}
console.log(isEnabled);
});
popup.js
const send = (s) => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
chrome.tabs.sendMessage(tabs[0].id, s);
});
}
document.getElementById("on").onclick = () => {
send("on");
}
document.getElementById("off").onclick = () => {
send("off");
}
popup.html
<html>
<body>
<button id="on">on</button>
<button id="off">off</button>
<script src="popup.js"></script>
</body>
</html>
For my chrome extension's options page I have two radio buttons to switch between showing and not showing the buttons on a page. The function works but it takes me a few reloads of the web page before the changes take effect. Does it take some time to save to chrome.storage before the changes take effect/can be used?
here is the JS for my options page:
const saveBtn = document.getElementById('save-btn');
saveBtn.addEventListener('click', () => {
if(document.getElementById('show_btns').checked){
console.log('true');
chrome.storage.sync.set({ btn_disp: true });
} else {
console.log('false');
chrome.storage.sync.set({ btn_disp: false });
}
});
Background script waits for page to load then injects content scripts and css and sends user data with message.
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
//inject the script into page
//only inject on complete load
//listening for the 'complete' message from tabs
if(changeInfo.status === 'complete' && /^http/.test(tab.url)){
chrome.scripting.executeScript({
target: { tabId: tabId },
files: ['./src/js/foreground.js']
})
.then(() => {
console.log('INJECTED');
})
.catch(err => console.log(err));
chrome.scripting.insertCSS({
target: {tabId: tabId},
files: ['./src/css/page_btn_styles.css']
})
.then(() => {
console.log('page_btn_styles.css injected');
sendToForeground();
})
.catch(err => console.log(err));
}
});
//page loaded -> load buttons
function sendToForeground() {
chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, { message: 'page_loaded', data: user_data }, (response) => {
console.log('page_loaded message sent');
});
});
}
My content script listens for a message from the background script that the page has loaded. The background script sends the user data object with the message.
//message from background.js that page has loaded
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if(request.message === 'page_loaded'){
const allElements = document.querySelectorAll('*');
for(let i = 0; i < allElements.length; i++) {
if(allElements[i].className.toString().match(/ingredient/g)){
console.log(request.data);
loadPageBtns(request.data);
break;
}
}
}
sendResponse({});
});
JS for loading page buttons in the content script
function loadPageBtns(user_data) {
if(user_data.btn_disp){
//container for buttons on page
let container = document.createElement('div');
container.id = 'btns-container';
//ingredients button
let ingredientsBtn = document.createElement('button');
ingredientsBtn.id = 'ingredients-btn';
ingredientsBtn.classList.add('page-btns');
ingredientsBtn.classList.add('animated');
ingredientsBtn.classList.add('bounceInLeft');
ingredientsBtn.innerText = 'Get Ingredients';
ingredientsBtn.addEventListener('click', () => {
scrollToIngredients();
});
ingredientsBtn.addEventListener('mouseover', () => {
ingredientsBtn.classList.remove('animated');
});
//load event handler for click on recipe
//recipe button
let recipesBtn = document.createElement('button');
recipesBtn.id = 'recipes-btn';
recipesBtn.classList.add('page-btns');
recipesBtn.classList.add('animated');
recipesBtn.classList.add('bounceInLeft');
recipesBtn.innerText = 'Get Recipe';
recipesBtn.addEventListener('click', () => {
scrollToDirections();
});
recipesBtn.addEventListener('mouseover', () => {
recipesBtn.classList.remove('animated');
});
container.appendChild(ingredientsBtn);
container.appendChild(recipesBtn);
document.body.appendChild(container);
}
}
Chrome storage does not seem to save anything when the data is false, i.e. chrome.storage.sync.set({ btn_disp: false });
does not store anything. If it is true then it does store the item.
This can be a problem depending on your default value. If you have a default of TRUE then you end up never being able to store a FALSE value.
I resolved this by storing text true or false rather then boolean.
I can't see your retrieve and default logic so I'm not sure if this is the root of your problem.
Hi so as you can see i have a problem with videojs-recorder, problem is i can't get access to my cam on phone. I use code down below and i am testing it with remote dev tools on chrome. Error is:
VIDEOJS: ERROR: TypeError: Cannot read property 'getUserMedia' of undefined
This is my code:
let player;
const options = {
controls: true,
width: 300,
height: 300,
plugins: {
record: {
audio: true,
screen: true,
image: false,
maxLength: 3600,
debug: true,
}
}
};
player = videojs("myVideo", options , function(){
// print version information at startup
videojs.log('Using video.js', videojs.VERSION,
'with videojs-record', videojs.getPluginVersion('record'),
'and recordrtc', RecordRTC.version);
});
// error handling
player.on('deviceError', function() {
console.log('device error:', player.deviceErrorCode);
});
player.on('error', function(element, error) {
console.error(error);
});
player.on('startRecord', function() {
console.log('started recording!');
});
player.on('finishRecord', function() {
console.log('finished recording: ', player.recordedData);
});
Edit:
Currently i am using 2.1.2 and i am unable to do update.
I am creating a image download server for my APP with Rest API, there are two buttons in the HTML, one is download button and other is load button.
When clicked on download button progress bar shows the progress, but there is no output file and doesn't show any error even load button doesn't load anything.
This script works fine with static URL (without API) by making certain changes in the script.
Link for my temporary Server.
http://freaksearch.com/aarti/rest-api.php?json=image&Id=
Plugin required by this script : cordova-plugin-file-transfer
(this is not depreciated plugin)
Link for the plugin: https://www.npmjs.com/package/cordova-plugin-file-transfer
Here is my HTML:
<div class="padding">
<button class="button" ng-click="download()">Download</button>
<button class="button" ng-click="load()">Load</button>
{{imgFile}}
<img ng-src="{{imgFile}}">
</div>
Here is my script:
$scope.download = function(imageId, imageName) {
$ionicLoading.show({
template: 'Downloading...'
});
window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function (fs) {
fs.root.getDirectory(
"MyProject",
{
create: true
},
function (dirEntry) {
dirEntry.getFile(
imageName + ".jpg",
{
create: true,
exclusive: false
},
function gotFileEntry(fe) {
var p = fe.toURL();
fe.remove();
ft = new FileTransfer();
ft.download(
encodeURI('http://freaksearch.com/aarti/rest-api?json=image' + imageId),
p,
function (entry) {
$ionicLoading.hide();
$scope.imgFile = entry.toURL();
},
function (error) {
$ionicLoading.hide();
alert("Download Error Source --> " + error.source);
},
false,
null
);
},
function () {
$ionicLoading.hide();
console.log("Get the file failed");
}
);
}
);
},
function () {
$ionicLoading.hide();
console.log("Request for filesystem failed");
});
}
$scope.load = function() {
$ionicLoading.show({
template: 'Loading...'
});
window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function(fs) {
fs.root.getDirectory(
"MyProject",
{
create: false
},
function(dirEntry) {
dirEntry.getFile(
imageName + ".jpg",
{
create: false,
exclusive: false
},
function gotFileEntry(fe) {
$ionicLoading.hide();
$scope.imgFile = fe.toURL();
},
function(error) {
$ionicLoading.hide();
console.log("Error getting file");
}
);
}
);
},
function() {
$ionicLoading.hide();
console.log("Error requesting filesystem");
});
}
Adding <preference name="AndroidPersistentFileLocation" value="Compatibility" />to the config.xml made my folder & file visible. I hope this could help someone.
Now i can see the images, but all the images are Corrupt.
I have my Angular view file like below.
<!DOCTYPE html>
<video id="myVideo" class="video-js vjs-default-skin"></video>
<script>
var dataUri;
var videoData;
var player = videojs("myVideo", {
controls: true,
width: 320,
height: 240,
fluid: false,
plugins: {
record: {
audio: true,
video: true,
maxLength: 100,
debug: true
}
}
}, function(){
// print version information at startup
videojs.log('Using video.js', videojs.VERSION,
'with videojs-record', videojs.getPluginVersion('record'),
'and recordrtc', RecordRTC.version);
});
// error handling
player.on('deviceError', function() {
console.log('device error:', player.deviceErrorCode);
});
player.on('error', function(error) {
console.log('error:', error);
});
// user clicked the record button and started recording
player.on('startRecord', function() {
console.log('started recording!');
});
// user completed recording and stream is available
player.on('finishRecord', function() {
console.log('player : ', player.recordedData.video.name);
videoData = player.recordedData;
console.log('finished recording: ', player.recordedData);
}
);
function getVideoData()
{
return videoData;
}
</script>
<button id="record" onClick="getVideoData();" ng-model="onFileSelect()"></button>
When player.on('finishRecord', function() function is called it will have the recorded video data in player.recordedData variable. What My problem is, I want to send the player.recordedData to the angular controller on button click whose id is record.
If the vairiable is defined globally, you can directly use it in any of controllers. Try to put you data in object.xxx format.
example:
var model = {videoData: null};
player.on('finishRecord', function() {
...
model.videoData = player.recordedData;
}
in controller:
//directly use it, ensure it has data
model.videoData