Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support taking video screenshots outside of Electron #6399

Merged
merged 1 commit into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 51 additions & 67 deletions src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import {
import {
addKeyboardShortcutToActionTitle,
getPicturesPath,
showSaveDialog,
showToast
showToast,
writeFileWithPicker
} from '../../helpers/utils'
import { pathExists } from '../../helpers/filesystem'

Expand Down Expand Up @@ -785,7 +785,7 @@ export default defineComponent({

uiConfig.controlPanelElements.push('fullscreen')

if (!process.env.IS_ELECTRON || !enableScreenshot.value || props.format === 'audio') {
if (!enableScreenshot.value || props.format === 'audio') {
const index = elementList.indexOf('ft_screenshot')
elementList.splice(index, 1)
}
Expand Down Expand Up @@ -1515,8 +1515,6 @@ export default defineComponent({
// #region screenshots

async function takeScreenshot() {
// TODO: needs to be refactored to be less reliant on node stuff, so that it can be used in the web (and android) builds

const video_ = video.value

const width = video_.videoWidth
Expand All @@ -1541,7 +1539,7 @@ export default defineComponent({
let filename
try {
filename = await store.dispatch('parseScreenshotCustomFileName', {
date: new Date(Date.now()),
date: new Date(),
playerTime: video_.currentTime,
videoId: props.videoId
})
Expand All @@ -1552,59 +1550,48 @@ export default defineComponent({
return
}

let subDir = ''
if (filename.indexOf(path.sep) !== -1) {
const lastIndex = filename.lastIndexOf(path.sep)
subDir = filename.substring(0, lastIndex)
filename = filename.substring(lastIndex + 1)
}
const filenameWithExtension = `${filename}.${format}`

let dirPath
let filePath
if (screenshotAskPath.value) {
if (!process.env.IS_ELECTRON || screenshotAskPath.value) {
const wasPlaying = !video_.paused
if (wasPlaying) {
video_.pause()
}

if (screenshotFolder.value === '' || !(await pathExists(screenshotFolder.value))) {
dirPath = await getPicturesPath()
} else {
dirPath = screenshotFolder.value
}
try {
/** @type {Blob} */
const blob = await new Promise((resolve) => canvas.toBlob(resolve, mimeType, imageQuality))

const saved = await writeFileWithPicker(
filenameWithExtension,
blob,
format.toUpperCase(),
mimeType,
`.${format}`,
'player-screenshots',
'pictures'
)

const options = {
defaultPath: path.join(dirPath, filenameWithExtension),
filters: [
{
name: format.toUpperCase(),
extensions: [format]
}
]
if (saved) {
showToast(t('Screenshot Success'))
}
} catch (error) {
console.error(error)
showToast(t('Screenshot Error', { error }))
}

const response = await showSaveDialog(options)
canvas.remove()

if (wasPlaying) {
video_.play()
}
if (response.canceled || response.filePath === '') {
canvas.remove()
return
}

filePath = response.filePath
if (!filePath.endsWith(`.${format}`)) {
filePath = `${filePath}.${format}`
}

dirPath = path.dirname(filePath)
store.dispatch('updateScreenshotFolderPath', dirPath)
} else {
let dirPath

if (screenshotFolder.value === '') {
dirPath = path.join(await getPicturesPath(), 'Freetube', subDir)
dirPath = path.join(await getPicturesPath(), 'Freetube')
} else {
dirPath = path.join(screenshotFolder.value, subDir)
dirPath = screenshotFolder.value
}

if (!(await pathExists(dirPath))) {
Expand All @@ -1617,24 +1604,25 @@ export default defineComponent({
return
}
}
filePath = path.join(dirPath, filenameWithExtension)
}

canvas.toBlob((result) => {
result.arrayBuffer().then(ab => {
const arr = new Uint8Array(ab)
const filePath = path.join(dirPath, filenameWithExtension)

fs.writeFile(filePath, arr)
.then(() => {
showToast(t('Screenshot Success', { filePath }))
})
.catch((err) => {
console.error(err)
showToast(t('Screenshot Error', { error: err }))
})
})
}, mimeType, imageQuality)
canvas.remove()
canvas.toBlob((result) => {
result.arrayBuffer().then(ab => {
const arr = new Uint8Array(ab)

fs.writeFile(filePath, arr)
.then(() => {
showToast(t('Screenshot Success'))
})
.catch((err) => {
console.error(err)
showToast(t('Screenshot Error', { error: err }))
})
})
}, mimeType, imageQuality)
canvas.remove()
}
}

// #endregion screenshots
Expand Down Expand Up @@ -1795,10 +1783,8 @@ export default defineComponent({

shakaContextMenu.registerElement('ft_stats', null)

if (process.env.IS_ELECTRON) {
shakaControls.registerElement('ft_screenshot', null)
shakaOverflowMenu.registerElement('ft_screenshot', null)
}
shakaControls.registerElement('ft_screenshot', null)
shakaOverflowMenu.registerElement('ft_screenshot', null)
}

// #endregion custom player controls
Expand Down Expand Up @@ -2168,7 +2154,7 @@ export default defineComponent({
}
break
case KeyboardShortcuts.VIDEO_PLAYER.GENERAL.TAKE_SCREENSHOT:
if (process.env.IS_ELECTRON && enableScreenshot.value && props.format !== 'audio') {
if (enableScreenshot.value && props.format !== 'audio') {
event.preventDefault()
// Take screenshot
takeScreenshot()
Expand Down Expand Up @@ -2370,9 +2356,7 @@ export default defineComponent({
return
}

if (process.env.IS_ELECTRON) {
registerScreenshotButton()
}
registerScreenshotButton()
registerAudioTrackSelection()
registerTheatreModeButton()
registerFullWindowButton()
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/player-settings/player-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ export default defineComponent({
getScreenshotFilenameExample: function(pattern) {
return this.parseScreenshotCustomFileName({
pattern: pattern || this.screenshotDefaultPattern,
date: new Date(Date.now()),
date: new Date(),
playerTime: 123.456,
videoId: 'dQw4w9WgXcQ'
}).then(res => {
Expand Down
11 changes: 4 additions & 7 deletions src/renderer/components/player-settings/player-settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -165,16 +165,14 @@
/>
</ft-flex-box>
<br>
<ft-flex-box
v-if="usingElectron"
>
<ft-flex-box>
<ft-toggle-switch
:label="$t('Settings.Player Settings.Screenshot.Enable')"
:default-value="enableScreenshot"
@change="updateEnableScreenshot"
/>
</ft-flex-box>
<div v-if="usingElectron && enableScreenshot">
<div v-if="enableScreenshot">
<ft-flex-box>
<ft-select
:placeholder="$t('Settings.Player Settings.Screenshot.Format Label')"
Expand All @@ -195,7 +193,7 @@
@change="updateScreenshotQuality"
/>
</ft-flex-box>
<ft-flex-box>
<ft-flex-box v-if="usingElectron">
<ft-toggle-switch
:label="$t('Settings.Player Settings.Screenshot.Ask Path')"
:default-value="screenshotAskPath"
Expand Down Expand Up @@ -223,7 +221,6 @@
/>
</ft-flex-box>
<ft-flex-box
v-if="usingElectron"
class="screenshotFolderContainer"
>
<p class="screenshotFilenamePatternTitle">
Expand All @@ -245,7 +242,7 @@
/>
<ft-input
class="screenshotFilenamePatternExample"
:placeholder="`${screenshotFilenameExample}`"
:placeholder="screenshotFilenameExample"
:show-action-button="false"
:show-label="false"
:disabled="true"
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/store/modules/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ const state = {
enableScreenshot: false,
screenshotFormat: 'png',
screenshotQuality: 95,
screenshotAskPath: false,
screenshotAskPath: !process.env.IS_ELECTRON,
screenshotFolderPath: '',
screenshotFilenamePattern: '%Y%M%D-%H%N%S',
settingsSectionSortEnabled: false,
Expand Down
60 changes: 25 additions & 35 deletions src/renderer/store/modules/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,44 +276,34 @@ const actions = {
},

parseScreenshotCustomFileName: function({ rootState }, payload) {
return new Promise((resolve, reject) => {
const { pattern = rootState.settings.screenshotFilenamePattern, date, playerTime, videoId } = payload
const keywords = [
['%Y', date.getFullYear()], // year 4 digits
['%M', (date.getMonth() + 1).toString().padStart(2, '0')], // month 2 digits
['%D', date.getDate().toString().padStart(2, '0')], // day 2 digits
['%H', date.getHours().toString().padStart(2, '0')], // hour 2 digits
['%N', date.getMinutes().toString().padStart(2, '0')], // minute 2 digits
['%S', date.getSeconds().toString().padStart(2, '0')], // second 2 digits
['%T', date.getMilliseconds().toString().padStart(3, '0')], // millisecond 3 digits
['%s', parseInt(playerTime)], // video position second n digits
['%t', (playerTime % 1).toString().slice(2, 5) || '000'], // video position millisecond 3 digits
['%i', videoId] // video id
]

let parsedString = pattern
for (const [key, value] of keywords) {
parsedString = parsedString.replaceAll(key, value)
}

if (parsedString !== replaceFilenameForbiddenChars(parsedString)) {
reject(new Error(i18n.t('Settings.Player Settings.Screenshot.Error.Forbidden Characters')))
}
const { pattern = rootState.settings.screenshotFilenamePattern, date, playerTime, videoId } = payload
const keywords = [
['%Y', date.getFullYear()], // year 4 digits
['%M', (date.getMonth() + 1).toString().padStart(2, '0')], // month 2 digits
['%D', date.getDate().toString().padStart(2, '0')], // day 2 digits
['%H', date.getHours().toString().padStart(2, '0')], // hour 2 digits
['%N', date.getMinutes().toString().padStart(2, '0')], // minute 2 digits
['%S', date.getSeconds().toString().padStart(2, '0')], // second 2 digits
['%T', date.getMilliseconds().toString().padStart(3, '0')], // millisecond 3 digits
['%s', parseInt(playerTime)], // video position second n digits
['%t', (playerTime % 1).toString().slice(2, 5) || '000'], // video position millisecond 3 digits
['%i', videoId] // video id
]

let parsedString = pattern
for (const [key, value] of keywords) {
parsedString = parsedString.replaceAll(key, value)
}

let filename
if (parsedString.indexOf(path.sep) !== -1) {
const lastIndex = parsedString.lastIndexOf(path.sep)
filename = parsedString.substring(lastIndex + 1)
} else {
filename = parsedString
}
if (parsedString !== replaceFilenameForbiddenChars(parsedString)) {
throw new Error(i18n.t('Settings.Player Settings.Screenshot.Error.Forbidden Characters'))
}

if (!filename) {
reject(new Error(i18n.t('Settings.Player Settings.Screenshot.Error.Empty File Name')))
}
if (!parsedString) {
throw new Error(i18n.t('Settings.Player Settings.Screenshot.Error.Empty File Name'))
}

resolve(parsedString)
})
return parsedString
},

showAddToPlaylistPromptForManyVideos ({ commit }, { videos: videoObjectArray, newPlaylistDefaultProperties }) {
Expand Down
3 changes: 1 addition & 2 deletions static/locales/be.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -441,8 +441,7 @@ Settings:
File Name Tooltip: 'Вы можаце выкарыстоўваць пераменныя ніжэй. %Y Год 4 лічбы.
%M Месяц 2 лічбы. %D Дні 2 лічбы. %H Гадзіны 2 лічбы. %N Хвіліны 2 лічбы.
%S Секунды 2 лічбы. %T Мілісекунды 3 лічбы. %s Секунды відэа. %t Мілісекунды
відэа 3 лічбы. %i Ідэнтыфікатар відэа. Вы таксама можаце выкарыстоўваць "\"
або "/" для стварэння падпапак.'
відэа 3 лічбы. %i Ідэнтыфікатар відэа.'
Error:
Forbidden Characters: 'Забароненыя сімвалы'
Empty File Name: 'Пустая назва файла'
Expand Down
3 changes: 1 addition & 2 deletions static/locales/bg.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -444,8 +444,7 @@ Settings:
File Name Tooltip: Можете да използвате променливите по-долу. %Y - година -
4 цифри. %M - месец - 2 цифри. %D - ден - 2 цифри. %H - час - 2 цифри. %N
- минути - 2 цифри. %S - секунди - 2 цифри. %T - милисекунди - 3 цифри. %s
- видеосекунди. %t - видео милисекунди - 3 цифри. %i ID на видеото. Можете
също така да използвате "\" или "/", за създаване на подпапки.
- видеосекунди. %t - видео милисекунди - 3 цифри. %i ID на видеото.
Format Label: Формат
Folder Button: Избор на папка
Enter Fullscreen on Display Rotate: Режим на цял екран при завъртане на дисплея
Expand Down
3 changes: 1 addition & 2 deletions static/locales/br.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -502,8 +502,7 @@ Settings:
File Name Tooltip: 'Gallout a rit implijout an argemmennoù da-heul. %Y Bloavezh
4 sifr. %M Miz 2 sifr. %D Devezh 2 sifr. %H Eur 2 sifr. %N Munutennoù 2 sifr.
%S Eilennoù 2 sifr. %T Mili-eilennoù 3 sifr. %s Eilennoù ar video. %t Mili-eilennoù
ar video 3 sifr. %i ID ar video. Gallout a rit implijout ivez \ pe / evit
krouiñ teuliadoù.'
ar video 3 sifr. %i ID ar video.'
Error:
Forbidden Characters: 'Arouezennoù difennet'
Empty File Name: 'Goullo eo anv ar restr'
Expand Down
2 changes: 1 addition & 1 deletion static/locales/cs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ Settings:
File Name Tooltip: Můžete použít proměnné níže. %Y Rok 4 číslice. %M Měsíc 2
číslice. %D Den 2 číslice. %H Hodina 2 číslice. %N Minuta 2 číslice. %S Sekunda
2 číslice. %T Milisekunda 3 číslice. %s Sekunda videa. %t Milisekunda videa
3 číslice. %i ID videa. Můžete také použít "\" nebo "/" pro vytvoření podsložek.
3 číslice. %i ID videa.
Ask Path: Zeptat se na složku pro uložení
Folder Label: Složka snímků obrazovky
Enable: Povolit snímek obrazovky
Expand Down
2 changes: 1 addition & 1 deletion static/locales/cy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ Settings:
File Name Tooltip: 'Gallwch ddefnyddio newidynnau isod. %Y Blwyddyn 4 digid.
% M Mis 2 ddigid. %D Diwrnod 2 ddigid. % H Awr 2 ddigid. % N Munud 2 ddigid.
%S Ail 2 ddigid. % T Millisecond 3 digid. %s Eiliad Fideo. %t Fideo Millisecond
3 digid. %i ID fideo. Gallwch hefyd ddefnyddio "\" neu "/" i greu is-ffolderi.'
3 digid. %i ID fideo.'
Error:
Forbidden Characters: 'Nodau Gwahardd'
Empty File Name: 'Enw Ffeil Gwag'
Expand Down
3 changes: 1 addition & 2 deletions static/locales/da.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,7 @@ Settings:
Empty File Name: Tomt Filnavn
File Name Tooltip: Du kan bruge variabler nedenfor. %Y År 4 tal. %M Måned 2
tal. %D Dag 2 tal. %H Time 2 tal. %N Minut 2 tal. %S Sekund 2 tal. %T Millisekund
3 tal. %s Video-sekund. %t Video-millisekund 3 tal. %i Video-ID. Du kan også
bruge "\" eller "/" til at oprette undermapper.
3 tal. %s Video-sekund. %t Video-millisekund 3 tal. %i Video-ID.
Ask Path: Spørg efter Mappe til at Gemme
Folder Label: Mappe til Skærmbilleder
Next Video Interval: Næste Videointerval
Expand Down
3 changes: 1 addition & 2 deletions static/locales/de-DE.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -450,8 +450,7 @@ Settings:
File Name Tooltip: Du kannst die folgenden Variablen verwenden. %Y Jahr 4-stellig.
%M Monat 2 Ziffern. %D Tag 2 Ziffern. %H Stunde 2 Ziffern. %N Minute 2 Ziffern.
%S Sekunde 2 Ziffern. %T Millisekunde 3 Ziffern. %s Video-Sekunde. %t Video
Millisekunde 3 Ziffern. %i Video-ID. Du kannst auch \ oder / verwenden, um
Unterordner zu erstellen.
Millisekunde 3 Ziffern. %i Video-ID.
Enter Fullscreen on Display Rotate: Beim Drehen des Bildschirms zu Vollbild wechseln
Skip by Scrolling Over Video Player: Überspringen durch Scrollen über den Videoabspieler
Autoplay Interruption Timer: Automatische Wiedergabe Unterbrechungs-Timer
Expand Down
Loading
Loading