Skip to content

Commit eb0f06c

Browse files
committed
feat: Fallback to manually loading media on error
This is needed for E2EE files that will error when loading them directly from the HTML element's src attribute. Signed-off-by: Louis Chemineau <louis@chmn.me>
1 parent 3ed79f3 commit eb0f06c

File tree

4 files changed

+91
-8
lines changed

4 files changed

+91
-8
lines changed

src/components/Audios.vue

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
<template>
77
<!-- Plyr currently replaces the parent. Wrapping to prevent this
88
https://github.com/redxtech/vue-plyr/issues/259 -->
9-
<div v-if="src">
9+
<div v-if="url">
1010
<VuePlyr ref="plyr"
1111
:options="options">
1212
<audio ref="audio"
1313
:autoplay="active"
14-
:src="src"
14+
:src="url"
1515
preload="metadata"
16+
@error.capture.prevent.stop.once="onFail"
1617
@ended="donePlaying"
1718
@canplay="doneLoading">
1819

@@ -28,20 +29,32 @@
2829
</div>
2930
</template>
3031

31-
<script>
32+
<script lang='ts'>
33+
import Vue from 'vue'
34+
import AsyncComputed from 'vue-async-computed'
3235
// eslint-disable-next-line n/no-missing-import
3336
import '@skjnldsv/vue-plyr/dist/vue-plyr.css'
37+
3438
import logger from '../services/logger.js'
39+
import { preloadMedia } from '../services/mediaPreloader'
3540
3641
const VuePlyr = () => import(/* webpackChunkName: 'plyr' */'@skjnldsv/vue-plyr')
3742
43+
Vue.use(AsyncComputed)
44+
3845
export default {
3946
name: 'Audios',
4047
4148
components: {
4249
VuePlyr,
4350
},
4451
52+
data() {
53+
return {
54+
fallback: false,
55+
}
56+
},
57+
4558
computed: {
4659
player() {
4760
return this.$refs.plyr.player
@@ -57,6 +70,16 @@ export default {
5770
},
5871
},
5972
73+
asyncComputed: {
74+
async url(): Promise<string> {
75+
if (this.fallback) {
76+
return preloadMedia(this.filename)
77+
} else {
78+
return this.src
79+
}
80+
},
81+
},
82+
6083
watch: {
6184
active(val, old) {
6285
// the item was hidden before and is now the current view
@@ -94,6 +117,14 @@ export default {
94117
this.$refs.audio.autoplay = false
95118
this.$refs.audio.load()
96119
},
120+
121+
// Fallback to the original image if not already done
122+
onFail() {
123+
if (!this.fallback) {
124+
console.error(`Loading of file ${this.filename} failed, falling back to fetching it by hand`)
125+
this.fallback = true
126+
}
127+
},
97128
},
98129
}
99130
</script>

src/components/Images.vue

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
</div>
7373
</template>
7474

75-
<script>
75+
<script lang="ts">
7676
import Vue from 'vue'
7777
import AsyncComputed from 'vue-async-computed'
7878
import PlayCircleOutline from 'vue-material-design-icons/PlayCircleOutline.vue'
@@ -85,6 +85,7 @@ import { NcLoadingIcon } from '@nextcloud/vue'
8585
import ImageEditor from './ImageEditor.vue'
8686
import { findLivePhotoPeerFromFileId } from '../utils/livePhotoUtils'
8787
import { getDavPath } from '../utils/fileUtils'
88+
import { preloadMedia } from '../services/mediaPreloader'
8889
8990
Vue.use(AsyncComputed)
9091
@@ -181,7 +182,12 @@ export default {
181182
// If there is no preview and we have a direct source
182183
// load it instead
183184
if (this.source && !this.hasPreview && !this.previewUrl) {
184-
return this.source
185+
// If loading the source failed once, let's try fetching it by had
186+
if (this.fallback) {
187+
return preloadMedia(this.filename)
188+
} else {
189+
return this.source
190+
}
185191
}
186192
187193
// If loading the preview failed once, let's load the original file

src/components/Videos.vue

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<template>
77
<!-- Plyr currently replaces the parent. Wrapping to prevent this
88
https://github.com/redxtech/vue-plyr/issues/259 -->
9-
<div v-if="src">
9+
<div v-if="url">
1010
<VuePlyr ref="plyr"
1111
:options="options"
1212
:style="{
@@ -17,8 +17,9 @@
1717
:autoplay="active ? true : null"
1818
:playsinline="true"
1919
:poster="livePhotoPath"
20-
:src="src"
20+
:src="url"
2121
preload="metadata"
22+
@error.capture.prevent.stop.once="onFail"
2223
@ended="donePlaying"
2324
@canplay="doneLoading"
2425
@loadedmetadata="onLoadedMetadata">
@@ -35,18 +36,25 @@
3536
</div>
3637
</template>
3738

38-
<script>
39+
<script lang='ts'>
3940
// eslint-disable-next-line n/no-missing-import
41+
import Vue from 'vue'
42+
import AsyncComputed from 'vue-async-computed'
4043
import '@skjnldsv/vue-plyr/dist/vue-plyr.css'
44+
4145
import { imagePath } from '@nextcloud/router'
46+
4247
import logger from '../services/logger.js'
4348
import { findLivePhotoPeerFromName } from '../utils/livePhotoUtils'
4449
import { getPreviewIfAny } from '../utils/previewUtils'
50+
import { preloadMedia } from '../services/mediaPreloader.js'
4551
4652
const VuePlyr = () => import(/* webpackChunkName: 'plyr' */'@skjnldsv/vue-plyr')
4753
4854
const blankVideo = imagePath('viewer', 'blank.mp4')
4955
56+
Vue.use(AsyncComputed)
57+
5058
export default {
5159
name: 'Videos',
5260
@@ -56,6 +64,7 @@ export default {
5664
data() {
5765
return {
5866
isFullscreenButtonVisible: false,
67+
fallback: false,
5968
}
6069
},
6170
@@ -86,6 +95,16 @@ export default {
8695
},
8796
},
8897
98+
asyncComputed: {
99+
async url(): Promise<string> {
100+
if (this.fallback) {
101+
return preloadMedia(this.filename)
102+
} else {
103+
return this.src
104+
}
105+
},
106+
},
107+
89108
watch: {
90109
active(val, old) {
91110
// the item was hidden before and is now the current view
@@ -155,6 +174,14 @@ export default {
155174
this.player.stop()
156175
}
157176
},
177+
178+
// Fallback to the original image if not already done
179+
onFail() {
180+
if (!this.fallback) {
181+
console.error(`Loading of file ${this.filename} failed, falling back to fetching it by hand`)
182+
this.fallback = true
183+
}
184+
},
158185
},
159186
}
160187
</script>

src/services/mediaPreloader.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
/* eslint-disable jsdoc/require-jsdoc */
7+
8+
import type { ResponseDataDetailed, WebDAVClient } from 'webdav'
9+
10+
import { getClient, getRootPath } from '@nextcloud/files/dav'
11+
12+
// Manually load a WebDAV media from its filename, then expose the received Blob as an object URL.
13+
// This is needed for E2EE files that will error when loading them directly from the HTML element's src attribute.
14+
// Can be removed if we ever move the E2EE proxy to a service worker.
15+
export async function preloadMedia(filename: string): Promise<string> {
16+
const client = getClient() as WebDAVClient
17+
const response = await client.getFileContents(`${getRootPath()}${filename}`, { details: true }) as ResponseDataDetailed<ArrayBuffer>
18+
return URL.createObjectURL(new Blob([response.data], { type: response.headers['content-type'] }))
19+
}

0 commit comments

Comments
 (0)