Skip to content
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
55 changes: 50 additions & 5 deletions __tests__/component/media.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -613,8 +613,8 @@ describe('a video that has played to the end', () => {
// What a user puts beside a film to show before and after it plays:
// a picture of the same name in the same folder
async function mountWithPoster() {
const movie = makeFile({ basename: 'trailer.webm', mime: 'video/webm' })
const poster = makeFile({ basename: 'trailer.jpg', mime: 'image/jpeg' })
const movie = makeFile({ id: 10, basename: 'trailer.webm', mime: 'video/webm', attributes: { 'metadata-files-live-photo': 11 } })
const poster = makeFile({ id: 11, basename: 'trailer.jpg', mime: 'image/jpeg', attributes: { 'metadata-files-live-photo': 10 } })
const wrapper = mount(Videos, { props: makeProps({ file: movie, files: [movie, poster] }) })
await flushPromises()
const video = wrapper.find('video').element as HTMLVideoElement
Expand All @@ -628,6 +628,49 @@ describe('a video that has played to the end', () => {
expect(wrapper.find('video').attributes('poster')).toBe(poster.encodedSource)
})

// The dav metadata links the halves by file id, so a renamed half keeps
// its partner while a name that merely looks alike is not one.
it('keeps the poster of a live photo whose picture was renamed', async () => {
const movie = makeFile({ id: 20, basename: 'trailer.webm', mime: 'video/webm', attributes: { 'metadata-files-live-photo': 21 } })
const renamed = makeFile({ id: 21, basename: 'holidays.jpg', mime: 'image/jpeg', attributes: { 'metadata-files-live-photo': 20 } })
const wrapper = mount(Videos, { props: makeProps({ file: movie, files: [movie, renamed] }) })
await flushPromises()

expect(wrapper.find('video').attributes('poster')).toBe(renamed.encodedSource)
})

it('ignores a picture of the same name that is not its live photo peer', async () => {
// The metadata points at a file that is not in the list: falling back
// to the name would show a picture that is not its peer.
const movie = makeFile({ id: 30, basename: 'trailer.webm', mime: 'video/webm', attributes: { 'metadata-files-live-photo': 32 } })
const other = makeFile({ id: 31, basename: 'trailer.jpg', mime: 'image/jpeg' })
const wrapper = mount(Videos, { props: makeProps({ file: movie, files: [movie, other] }) })
await flushPromises()

expect(wrapper.find('video').attributes('poster')).not.toBe(other.encodedSource)
})

// A camera saves the poster beside the video, without live photo metadata
it('shows the picture of the same name for a video without metadata', async () => {
const movie = makeFile({ id: 50, basename: 'camera.webm', mime: 'video/webm' })
const picture = makeFile({ id: 51, basename: 'camera.jpg', mime: 'image/jpeg' })
const wrapper = mount(Videos, { props: makeProps({ file: movie, files: [movie, picture] }) })
await flushPromises()

expect(wrapper.find('video').attributes('poster')).toBe(picture.encodedSource)
})

// The picture of a live photo belongs with its own video, so it cannot
// serve as the poster of a video that merely shares its name
it('does not show a picture of the same name that is another live photo', async () => {
const movie = makeFile({ id: 60, basename: 'camera.webm', mime: 'video/webm' })
const picture = makeFile({ id: 61, basename: 'camera.jpg', mime: 'image/jpeg', attributes: { 'metadata-files-live-photo': 99 } })
const wrapper = mount(Videos, { props: makeProps({ file: movie, files: [movie, picture] }) })
await flushPromises()

expect(wrapper.find('video').attributes('poster')).not.toBe(picture.encodedSource)
})

it('goes back to its poster without downloading the video again', async () => {
const { wrapper, video, store, poster } = await mountWithPoster()

Expand Down Expand Up @@ -706,9 +749,11 @@ describe('a video before it can play', () => {
expect(wrapper.find('video').attributes('poster')).toContain('/core/preview')
})

it('shows the picture of the same name rather than its preview', async () => {
const picture = makeFile({ basename: 'clip.jpg', mime: 'image/jpeg' })
const wrapper = await mountVideo({ beside: [picture] })
it('shows the picture of its live photo rather than its preview', async () => {
const picture = makeFile({ id: 41, basename: 'clip.jpg', mime: 'image/jpeg' })
const movie = makeFile({ id: 40, basename: 'clip.mp4', mime: 'video/mp4', attributes: { hasPreview: true, 'metadata-files-live-photo': 41 } })
const wrapper = mount(Videos, { props: makeProps({ file: movie, files: [movie, picture] }) })
await flushPromises()

expect(wrapper.find('video').attributes('poster')).toBe(picture.encodedSource)
})
Expand Down
8 changes: 8 additions & 0 deletions __tests__/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,14 @@ describe('livePhotoUtils.findLivePhotoPeerFromName', () => {
expect(findLivePhotoPeerFromName(photo, [photo, peer])).toBe(peer)
})

// The picture of a live photo is the moving video's other half, so a
// video must not borrow it as its poster just because the names match
it('does not pair a video with the picture of another live photo', () => {
const video = makeFile({ id: 1, basename: 'IMG_1234.mov', mime: 'video/quicktime' })
const otherLivePhoto = makeFile({ id: 2, basename: 'IMG_1234.jpg', attributes: { 'metadata-files-live-photo': 99 } })
expect(findLivePhotoPeerFromName(video, [video, otherLivePhoto])).toBeUndefined()
})

it('reads the whole name of a file that has no extension', () => {
const video = makeFile({ id: 1, basename: 'clip', mime: 'video/quicktime' })
const longer = makeFile({ id: 2, basename: 'clips.jpg' })
Expand Down
11 changes: 9 additions & 2 deletions lib/components/Videos.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ import { preloadImageSize } from '../services/mediaPreloader.ts'
import { canDownload } from '../utils/canDownload.ts'
import { probeFile } from '../utils/isoMedia.ts'
import { t } from '../utils/l10n.ts'
import { findLivePhotoPeerFromName } from '../utils/livePhotoUtils.ts'
import { findLivePhotoPeerFromFileId, findLivePhotoPeerFromName } from '../utils/livePhotoUtils.ts'
import { playerLanguage } from '../utils/playerTranslations.ts'
import { getPreviewIfAny, getServerPreview } from '../utils/previewUtils.ts'

Expand Down Expand Up @@ -98,8 +98,15 @@ const width = ref(0)
watch(() => props.maxHeight, updateVideoSize)
watch(() => props.maxWidth, updateVideoSize)

const metadataFilesLivePhoto = computed(() => props.file.attributes?.['metadata-files-live-photo'] as number | undefined)

const livePhotoPath = computed(() => {
const peerFile = findLivePhotoPeerFromName(props.file, props.files)
// The dav metadata links the halves of a live photo by file id. A video
// without it keeps the picture of the same name beside it as its poster,
// the way cameras save them.
const peerFile = metadataFilesLivePhoto.value !== undefined
? findLivePhotoPeerFromFileId(metadataFilesLivePhoto.value, props.files)
: findLivePhotoPeerFromName(props.file, props.files)
if (peerFile === undefined) {
return undefined
}
Expand Down
5 changes: 5 additions & 0 deletions lib/utils/livePhotoUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ function nameWithoutExtension(file: IFile): string {
* exactly: `IMG_1234.mov` belongs with `IMG_1234.jpg` and not with the
* `IMG_1239.jpg` next to it.
*
* Only a picture without live photo metadata qualifies: a picture that is
* half of a live photo of its own belongs with its own video, so showing it
* here would put someone else's poster on the video.
*
* @param referenceFile - The file whose peer live photo should be found by name
* @param fileList - The list of files to search within
*/
Expand All @@ -47,6 +51,7 @@ export function findLivePhotoPeerFromName(referenceFile: IFile, fileList: IFile[
// if same filename and extension is allowed
return comparedFile.source !== referenceFile.source
&& (nameWithoutExtension(comparedFile) === name && livePictureExtRegex.test(comparedFile.basename))
&& comparedFile.attributes?.['metadata-files-live-photo'] === undefined
})
}

Expand Down
Loading