Skip to content

Commit

Permalink
feature: Video Block (#119)
Browse files Browse the repository at this point in the history
# Changes

Adds Video Block that renders a video uploaded in DatoCMS with a cover
image, caption, support for subtitles and options to autoplay, mute and
loop. Features (also in block readme):

- Privacy first alternative to [Video Embed Block](../VideoEmbedBlock/),
as video uploaded in DatoCMS is served without tracking (no consent
required).
- Supports video streaming with adaptive bitrate (using HLS) for best UX
and performance.
- Fallback to mp4 video when streaming is not available.
- Fallback to video download link when HTML video element is not
supported.
- Supports subtitle tracks for enhanced accessibility, automatically
selecting default locale when available.
- Supports figcaption defaulting to external video's title and optional
custom title override.
- Supports autoplay, mute and loop.
- Autoplay is only triggered if no reduced motion is preferred (for
a11y) and save data mode is off.
- Conditionally loads video and streaming package (`hls.js`) on click or
when in view in case autoplay is enabled.

Moved [custom image and custom play button overlay to separate
ticket](#121).

# Associated issue

Resolves #33 

# How to test

1. Open
[`/en/overview-page/demos/video-block/`](https://feat-video-block.head-start.pages.dev/en/overview-page/demos/video-block/)
2. Test with different uploaded videos in CMS, with and without
subtitles
3. Verify streaming, mp4 and fallback work (by disabling one at a time
locally)
4. Verify autoplay and other block options work as expected

# Checklist

- [x] I have performed a self-review of my own code
- [x] I have made sure that my PR is easy to review (not too big,
includes comments)
- [x] I have made updated relevant documentation files (in project
README, docs/, etc)
- ~~I have added a decision log entry if the change affects the
architecture or changes a significant technology~~
- [x] I have notified a reviewer

<!-- Please strike through and check off all items that do not apply
(rather than removing them) -->
  • Loading branch information
jbmoelker authored Jan 29, 2024
1 parent 84182fb commit 5130cad
Show file tree
Hide file tree
Showing 18 changed files with 628 additions and 1 deletion.
293 changes: 293 additions & 0 deletions config/datocms/migrations/1706472108_videoBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
import { Client } from '@datocms/cli/lib/cma-client-node';

export default async function (client: Client) {
console.log('Create new models/block models');

console.log('Create block model "\uD83C\uDFAC Video Block" (`video_block`)');
await client.itemTypes.create(
{
// @ts-expect-error next-line DatoCMS auto-generated
id: 'QYfZyBzIRWKxA1MinIR0aQ',
name: '\uD83C\uDFAC Video Block',
api_key: 'video_block',
modular_block: true,
inverse_relationships_enabled: false,
},
{ skip_menu_item_creation: true }
);

console.log(
'Create model "\uD83D\uDD21 Video Text Track" (`video_text_track`)'
);
await client.itemTypes.create(
{
// @ts-expect-error next-line DatoCMS auto-generated
id: 'Us90isT5SgeXHuetcEj8eA',
name: '\uD83D\uDD21 Video Text Track',
api_key: 'video_text_track',
collection_appearance: 'table',
inverse_relationships_enabled: false,
},
{ skip_menu_item_creation: true }
);

console.log('Creating new fields/fieldsets');

console.log(
'Create Single asset field "Video" (`video_asset`) in block model "\uD83C\uDFAC Video Block" (`video_block`)'
);
await client.fields.create('QYfZyBzIRWKxA1MinIR0aQ', {
// @ts-expect-error next-line DatoCMS auto-generated
id: 'KdXhYelkQdaepb_wpK7yuw',
label: 'Video',
field_type: 'file',
api_key: 'video_asset',
validators: { required: {} },
appearance: { addons: [], editor: 'file', parameters: {} },
default_value: null,
});

console.log(
'Create Single-line string field "Title" (`title`) in block model "\uD83C\uDFAC Video Block" (`video_block`)'
);
await client.fields.create('QYfZyBzIRWKxA1MinIR0aQ', {
// @ts-expect-error next-line DatoCMS auto-generated
id: 'CYYGKtnXSkiSCTYMrOI3Yg',
label: 'Title',
field_type: 'string',
api_key: 'title',
hint: 'Optional title. By default the title from the selected video is used.',
appearance: {
addons: [],
editor: 'single_line',
parameters: { heading: false },
},
default_value: '',
});

console.log(
'Create Boolean field "Autoplay" (`autoplay`) in block model "\uD83C\uDFAC Video Block" (`video_block`)'
);
await client.fields.create('QYfZyBzIRWKxA1MinIR0aQ', {
// @ts-expect-error next-line DatoCMS auto-generated
id: 'SEULcq3NS06_pDVrsHxiUA',
label: 'Autoplay',
field_type: 'boolean',
api_key: 'autoplay',
hint: 'Note: video will only autoplay if device supports it and user has consented to 3rd party content.',
appearance: { addons: [], editor: 'boolean', parameters: {} },
default_value: null,
});

console.log(
'Create Boolean field "Mute" (`mute`) in block model "\uD83C\uDFAC Video Block" (`video_block`)'
);
await client.fields.create('QYfZyBzIRWKxA1MinIR0aQ', {
// @ts-expect-error next-line DatoCMS auto-generated
id: 'YaS-9DSqS3umFlirmXcOkw',
label: 'Mute',
field_type: 'boolean',
api_key: 'mute',
appearance: { addons: [], editor: 'boolean', parameters: {} },
default_value: null,
});

console.log(
'Create Boolean field "Loop" (`loop`) in block model "\uD83C\uDFAC Video Block" (`video_block`)'
);
await client.fields.create('QYfZyBzIRWKxA1MinIR0aQ', {
// @ts-expect-error next-line DatoCMS auto-generated
id: 'AsETx2yITvOK2QKbu8bYRw',
label: 'Loop',
field_type: 'boolean',
api_key: 'loop',
appearance: { addons: [], editor: 'boolean', parameters: {} },
default_value: null,
});

console.log(
'Create Multiple links field "Tracks" (`tracks`) in block model "\uD83C\uDFAC Video Block" (`video_block`)'
);
await client.fields.create('QYfZyBzIRWKxA1MinIR0aQ', {
// @ts-expect-error next-line DatoCMS auto-generated
id: 'WVz9yVpWSymoFxO4oKzzeA',
label: 'Tracks',
field_type: 'links',
api_key: 'tracks',
hint: 'For accessibility, videos should provide both captions and transcripts that accurately describe its content. Captions allow people who are experiencing hearing loss to understand a video\'s audio content as the video is being played, while transcripts allow people who need additional time to be able to review audio content at a pace and format that is comfortable for them.',
validators: {
items_item_type: {
on_publish_with_unpublished_references_strategy: 'fail',
on_reference_unpublish_strategy: 'delete_references',
on_reference_delete_strategy: 'delete_references',
item_types: ['Us90isT5SgeXHuetcEj8eA'],
},
},
appearance: { addons: [], editor: 'links_embed', parameters: {} },
default_value: null,
});

console.log(
'Create Single-line string field "Title" (`title`) in model "\uD83D\uDD21 Video Text Track" (`video_text_track`)'
);
await client.fields.create('Us90isT5SgeXHuetcEj8eA', {
// @ts-expect-error next-line DatoCMS auto-generated
id: 'GzfEVsO6QnOu2KRUaPhc5Q',
label: 'Title',
field_type: 'string',
api_key: 'title',
hint: 'A user-readable title of the text track which is used by the browser when listing available text tracks. Defaults to selected language name (English, Deutsch, etc) when empty.',
appearance: {
addons: [],
editor: 'single_line',
parameters: { heading: false },
},
default_value: '',
});

console.log(
'Create Single-line string field "Locale" (`locale`) in model "\uD83D\uDD21 Video Text Track" (`video_text_track`)'
);
await client.fields.create('Us90isT5SgeXHuetcEj8eA', {
// @ts-expect-error next-line DatoCMS auto-generated
id: 'AD-nA46KQc2wL8978d6asg',
label: 'Locale',
field_type: 'string',
api_key: 'locale',
validators: { required: {} },
appearance: {
addons: [],
editor: 'string_select',
parameters: {
options: [
{ hint: '', label: 'Deutsch', value: 'de' },
{ hint: '', label: 'English', value: 'en' },
{ hint: '', label: 'Espa\u00F1ol', value: 'es' },
{ hint: '', label: 'Fran\u00E7ais', value: 'fr' },
{ hint: '', label: 'Italiano', value: 'it' },
{ hint: '', label: 'Nederlands', value: 'nl' },
],
},
},
default_value: '',
});

console.log(
'Create Single-line string field "Kind" (`kind`) in model "\uD83D\uDD21 Video Text Track" (`video_text_track`)'
);
await client.fields.create('Us90isT5SgeXHuetcEj8eA', {
// @ts-expect-error next-line DatoCMS auto-generated
id: 'F7QOFgNLStmPAdNlSn7uuQ',
label: 'Kind',
field_type: 'string',
api_key: 'kind',
hint: 'How the text track is meant to be used.',
validators: {
required: {},
enum: {
values: [
'subtitles',
'captions',
'descriptions',
'chapters',
'metadata',
],
},
},
appearance: {
addons: [],
editor: 'single_line',
parameters: { heading: false },
},
default_value: 'subtitles',
});

console.log(
'Create Single asset field "File" (`file`) in model "\uD83D\uDD21 Video Text Track" (`video_text_track`)'
);
await client.fields.create('Us90isT5SgeXHuetcEj8eA', {
// @ts-expect-error next-line DatoCMS auto-generated
id: 'XNFlO-LWQJiMDd5r44PaKw',
label: 'File',
field_type: 'file',
api_key: 'file',
validators: { required: {}, extension: { extensions: ['vtt'] } },
appearance: { addons: [], editor: 'file', parameters: {} },
default_value: null,
});

console.log('Update existing fields/fieldsets');

console.log(
'Update Modular content field "Body" (`body_blocks`) in model "Home" (`home`)'
);
await client.fields.update('pUj2PObgTyC-8X4lvZLMBA', {
validators: {
rich_text_blocks: {
item_types: [
'BRbU6VwTRgmG5SbwUs0rBg',
'PAk40zGjQJCcDXXPgygUrA',
'QYfZyBzIRWKxA1MinIR0aQ',
'VZvVfu52RZK81WG0Dxp-FQ',
'V80liDVtRC-UYgd3Sm-dXg',
'ZdBokLsWRgKKjHrKeJzdpw',
'gezG9nO7SfaiWcWnp-HNqw',
'0SxYNS2CR1it_5LHYWuEQg',
],
},
},
});

console.log(
'Update Modular content field "Body" (`body_blocks`) in model "Page" (`page`)'
);
await client.fields.update('Q-z1nyMsQtC8Sr6w6J2oGw', {
validators: {
rich_text_blocks: {
item_types: [
'BRbU6VwTRgmG5SbwUs0rBg',
'PAk40zGjQJCcDXXPgygUrA',
'QYfZyBzIRWKxA1MinIR0aQ',
'VZvVfu52RZK81WG0Dxp-FQ',
'V80liDVtRC-UYgd3Sm-dXg',
'ZdBokLsWRgKKjHrKeJzdpw',
'gezG9nO7SfaiWcWnp-HNqw',
'0SxYNS2CR1it_5LHYWuEQg',
],
},
},
});

console.log(
'Update Structured text field "Text" (`text`) in block model "Text Block" (`text_block`)'
);
await client.fields.update('NtVXfZ6gTL2sKNffNeUf5Q', {
validators: {
required: {},
structured_text_blocks: {
item_types: [
'QYfZyBzIRWKxA1MinIR0aQ',
'ZdBokLsWRgKKjHrKeJzdpw',
'gezG9nO7SfaiWcWnp-HNqw',
'0SxYNS2CR1it_5LHYWuEQg',
],
},
structured_text_links: {
on_publish_with_unpublished_references_strategy: 'fail',
on_reference_unpublish_strategy: 'delete_references',
on_reference_delete_strategy: 'delete_references',
item_types: ['WywlzYXpSVWFQIeeNk3iMw'],
},
},
});

console.log('Finalize models/block models');

console.log(
'Update model "\uD83D\uDD21 Video Text Track" (`video_text_track`)'
);
await client.itemTypes.update('Us90isT5SgeXHuetcEj8eA', {
title_field: { id: 'AD-nA46KQc2wL8978d6asg', type: 'field' },
image_preview_field: { id: 'XNFlO-LWQJiMDd5r44PaKw', type: 'field' },
});
}
2 changes: 1 addition & 1 deletion datocms-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
* @see docs/getting-started.md on how to use this file
* @see docs/decision-log/2023-10-24-datocms-env-file.md on why file is preferred over env vars
*/
export const datocmsEnvironment = 'embed-block';
export const datocmsEnvironment = 'video-block';
export const datocmsBuildTriggerId = '30535';
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"datocms-structured-text-utils": "^2.0.4",
"get-video-id": "^3.6.5",
"globby": "^13.2.2",
"hls.js": "^1.5.2",
"html-validate": "^8.7.4",
"jiti": "^1.20.0",
"nanostores": "^0.9.5",
Expand Down
2 changes: 2 additions & 0 deletions src/blocks/Blocks.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import PagePartialBlock from './PagePartialBlock/PagePartialBlock.astro';
import TableBlock from './TableBlock/TableBlock.astro';
import TextBlock from './TextBlock/TextBlock.astro';
import TextImageBlock from './TextImageBlock/TextImageBlock.astro';
import VideoBlock from './VideoBlock/VideoBlock.astro';
import VideoEmbedBlock from './VideoEmbedBlock/VideoEmbedBlock.astro';
const blocksByTypename = {
Expand All @@ -15,6 +16,7 @@ const blocksByTypename = {
TableBlockRecord: TableBlock,
TextBlockRecord: TextBlock,
TextImageBlockRecord: TextImageBlock,
VideoBlockRecord: VideoBlock,
VideoEmbedBlockRecord: VideoEmbedBlock,
};
Expand Down
2 changes: 2 additions & 0 deletions src/blocks/Blocks.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
TableBlockFragment,
TextBlockFragment,
TextImageBlockFragment,
VideoBlockFragment,
VideoEmbedBlockFragment,
} from '@lib/types/datocms';

Expand All @@ -15,4 +16,5 @@ export type AnyBlock =
| TableBlockFragment
| TextBlockFragment
| TextImageBlockFragment
| VideoBlockFragment
| VideoEmbedBlockFragment;
4 changes: 4 additions & 0 deletions src/blocks/TextBlock/TextBlock.fragment.graphql
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import '@blocks/ImageBlock/ImageBlock.fragment.graphql'
#import '@blocks/InternalLink/InternalLink.fragment.graphql'
#import '@blocks/TableBlock/TableBlock.fragment.graphql'
#import '@blocks/VideoBlock/VideoBlock.fragment.graphql'
#import '@blocks/VideoEmbedBlock/VideoEmbedBlock.fragment.graphql'

fragment TextBlock on TextBlockRecord {
Expand All @@ -13,6 +14,9 @@ fragment TextBlock on TextBlockRecord {
... on TableBlockRecord {
...TableBlock
}
... on VideoBlockRecord {
...VideoBlock
}
... on VideoEmbedBlockRecord {
...VideoEmbedBlock
}
Expand Down
15 changes: 15 additions & 0 deletions src/blocks/VideoBlock/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# VideoBlock

**Renders a video uploaded in DatoCMS with a cover image, caption, support for subtitles and options to autoplay, mute and loop.**

## Features

- Privacy first alternative to [Video Embed Block](../VideoEmbedBlock/), as video uploaded in DatoCMS is served without tracking (no consent required).
- Supports video streaming with adaptive bitrate (using HLS) for best UX and performance.
- Fallback to mp4 video when streaming is not available.
- Fallback to video download link when HTML video element is not supported.
- Supports subtitle tracks for enhanced accessibility, automatically selecting default locale when available.
- Supports figcaption defaulting to external video's title and optional custom title override.
- Supports autoplay, mute and loop.
- Autoplay is only triggered if no reduced motion is preferred (for a11y) and save data mode is off.
- Conditionally loads video and streaming package (`hls.js`) on click or when in view in case autoplay is enabled.
Loading

0 comments on commit 5130cad

Please sign in to comment.