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

feat: video component #1131

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions jest-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@testing-library/jest-dom'
2 changes: 1 addition & 1 deletion jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ const config = {
// setupFiles: [],

// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
setupFilesAfterEnv: ['./jest-setup.js'], //setupFilesAfterEnv: ['<rootDir>/jest-setup.js']

// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
Expand Down
1 change: 0 additions & 1 deletion src/Accordion/AccordionHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export const AccordionHeader: FC<AccordionHeaderProps> = ({
return (
<div className='accordion-header' data-testid={testId}>
<Tag
data-bs-toggle='collapse'
aria-expanded={active ? 'true' : 'false'}
className={toggleClasses}
onClick={onToggle}
Expand Down
181 changes: 181 additions & 0 deletions src/Video/Video.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import React, { FC, useEffect } from 'react';

import { VideoPlayer } from 'bootstrap-italia';
import { CSSModule } from 'reactstrap/types/lib/utils';
import { Input, InputProps } from '../Input/Input';
import { Accordion } from '../Accordion/Accordion';
import { AccordionItem } from '../Accordion/AccordionItem';
import { AccordionHeader } from '../Accordion/AccordionHeader';
import { AccordionBody } from '../Accordion/AccordionBody';
import { Dimmer } from '../Dimmer/Dimmer';
import { DimmerButtons } from '../Dimmer/DimmerButtons';
import { Button } from '../Button/Button';
import { FormGroup, Label } from 'reactstrap';

export interface VideoSource {
src: string;
type: string;
}
export interface TrackSource {
kind: string;
src: string;
srcLang: string;
label: string;
isDefault?: boolean;
}
export interface transcription {
src: string;
type: 'video/mp4' | 'video/webm' | 'video/ogg';
}

export interface YouTubeVideo {
url: string;
hasDisclaimer?: boolean;
disclaimerText?: string;
disclaimerKey?: string;
}
export interface VideoProps extends InputProps {
innerRef?: React.Ref<HTMLInputElement>;
cssModule?: CSSModule;
sources?: Array<VideoSource>;
transcription?: string;
transcriptionLabel?: string;
tracks?: Array<TrackSource>;
poster?: string;
controls?: boolean;
autoPlay?: boolean;
loop?: boolean;
fluid?: boolean;
youtube?: YouTubeVideo;
}

export const Video: FC<VideoProps> = (props) => {
let vpInstance: VideoPlayer;
const [showTranscript, setShowTranscript] = React.useState(false);
const [showDisclaimer, setShowDisclaimer] = React.useState(false);
const [rememberFlag, setRememberFlag] = React.useState(false);
const [disclaimerText, setDisclaimerText] = React.useState(
`Accetta i cookie di YouTube per vedere il video. Puoi gestire le preferenze nella cookie policy.`
);

useEffect(() => {
const el = document.querySelector('video');
if (el && VideoPlayer) {
vpInstance = new VideoPlayer(el);
// setTimeout(() => {
// console.log(vpInstance.player.log); // Con .player puoi usare play(), stop() ecc ..
// }, 3000);
if (props.youtube?.url) {
loadYouTubeVideo(props.youtube.url, vpInstance);
if (props.youtube.hasDisclaimer) {
const serviceName = props.youtube.disclaimerKey || 'youtube';
const rememberFlag = localStorage.getItem(serviceName);
if (props.youtube.disclaimerText) {
setDisclaimerText(props.youtube.disclaimerText);
}
setRememberFlag(rememberFlag == 'true');
if (rememberFlag == 'true') {
setShowDisclaimer(false);
} else {
setShowDisclaimer(true);
}
}
}

if (props.autoPlay) {
setTimeout(() => {
vpInstance?.player?.play();
}, 1000);
}
}
return () => {
if (vpInstance) {
vpInstance.dispose();
}
};
}, []);

useEffect(() => {
if (props.youtube?.url) {
if (props.youtube.hasDisclaimer) {
const serviceName = props.youtube.disclaimerKey || 'youtube';
if (rememberFlag) {
// set cookie
localStorage.setItem(serviceName, 'true');
} else {
// reset cookie
localStorage.removeItem(serviceName);
}
}
}
}, [rememberFlag]);

function loadYouTubeVideo(url: string, vpInstance: VideoPlayer) {
if (vpInstance) {
vpInstance.setYouTubeVideo(url);
}
}

const { controls = true, autoPlay = false, loop = false, fluid = true, poster = undefined } = props;
const videoProps = { controls, autoPlay, loop, fluid, poster };

return (
<>
<div className='row dimmable'>
<video {...videoProps} preload='auto' className='video-js' data-bs-video={true}>
{props.sources?.map((source) => <source key={source.src} src={source.src} type={source.type} />)}
{props.tracks?.map((track) => {
const { src, kind, label, srcLang, isDefault } = track;
return (
<track
key={src}
kind={kind}
src={src}
srcLang={srcLang || ''}
label={label}
default={isDefault || false}
/>
);
})}
</video>
{props.transcription && (
<Accordion className='vjs-transcription'>
<AccordionItem>
<AccordionHeader active={showTranscript} onToggle={() => setShowTranscript((p) => !p)}>
{props.transcriptionLabel || 'Trascrizione'}
</AccordionHeader>
<AccordionBody active={showTranscript}>{props.transcription}</AccordionBody>
</AccordionItem>
</Accordion>
)}
</div>
<Dimmer icon='it-video' show={showDisclaimer} color='primary'>
<p dangerouslySetInnerHTML={{ __html: disclaimerText }}></p>
<DimmerButtons className='bg-primary'>
<Button
onClick={() => {
console.log('click');
setShowDisclaimer(false);
}}
color='primary'
>
Accetta
</Button>
<div className='d-flex align-items-center ml-2'>
<FormGroup check inline>
<Input
id='inline-checkbox'
type='checkbox'
checked={rememberFlag}
onChange={() => setRememberFlag((p) => !p)}
/>
<Label check for='inline-checkbox' defaultChecked={false} className='text-white'>
Ricorda per tutti i video
</Label>
</FormGroup>
</div>
</DimmerButtons>
</Dimmer>
</>
);
};
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export { Toolbar } from './Toolbar/Toolbar';
export { ToolbarDividerItem } from './Toolbar/ToolbarDividerItem';
export { ToolbarItem } from './Toolbar/ToolbarItem';
export { Transfer } from './Transfer/Transfer';
export { Video } from './Video/Video'

// Types
export type { AccordionProps } from './Accordion/Accordion';
Expand Down Expand Up @@ -258,6 +259,7 @@ export type { TimelineProps } from './Timeline/TimelineWrapper';
export type { ToggleProps } from './Toggle/Toggle';
export type { ToolbarProps } from './Toolbar/Toolbar';
export type { ToolbarItemBadge, ToolbarItemProps } from './Toolbar/ToolbarItem';
export type { VideoProps } from "./Video/Video"

export type {
BreadcrumbItemProps,
Expand Down
117 changes: 117 additions & 0 deletions stories/Components/Video.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';
import { Video } from '../../src';
import type { TrackSource } from '../../src/Video/Video';

const meta: Meta<typeof Video> = {
title: 'Documentazione/Componenti/Video',
component: Video,
parameters: {
docs: {
canvas: { sourceState: 'none' }
}
}
};

export default meta;

type Story = StoryObj<typeof Video>;

export const Trascription: Story = {
render: () => {
const sources = [
{ src: '//vjs.zencdn.net/v/oceans.mp4', type: 'video/mp4' },
{ src: '//vjs.zencdn.net/v/oceans.webm', type: 'video/webm' }
];
const transcription = 'Questa è la trascrizione testuale del video';

return (
<div className='bg-secondary container'>
<Video
sources={sources}
transcriptionLabel={'Mostra la trascrizione del video'}
transcription={transcription}
/>
</div>
);
}
};

export const Tracks: Story = {
render: () => {
const tracks: TrackSource[] = [
{
src: 'https://italia.github.io/bootstrap-italia/docs/assets/video/subtitles-it.vtt',
kind: 'subtitles',
label: 'Italiano',
srcLang: 'it',
isDefault: true
},
{
src: 'https://italia.github.io/bootstrap-italia/docs/assets/video/subtitles-en.vtt',
kind: 'subtitles',
label: 'English',
srcLang: 'en',
isDefault: false
},
{
kind: 'captions',
src: 'https://italia.github.io/bootstrap-italia/docs/assets/video/subtitles-es.vtt',
srcLang: 'es',
label: 'Español',
isDefault: false
}
];
const sources = [
{
src: 'https://italia.github.io/bootstrap-italia/docs/assets/video/ElephantsDreamDASH/ElephantsDream.mp4.mpd',
type: 'application/dash+xml'
}
];

const poster = 'https://italia.github.io/bootstrap-italia/docs/assets/video/ElephantsDream.mp4-poster21.jpg';
return (
<Video sources={sources} tracks={tracks} poster={poster} />
);
}
};

export const AutoplayAndControls: Story = {
render: () => {
const sources = [
{ src: '//vjs.zencdn.net/v/oceans.mp4', type: 'video/mp4' },
{ src: '//vjs.zencdn.net/v/oceans.webm', type: 'video/webm' }
];

return (
<Video sources={sources} autoPlay={true} fluid={true} controls={true} loop={true} />
);
}
};

export const YouTubeVideo: Story = {
render: () => {
const ytVideo = {
url: 'https://youtu.be/_0j7ZQ67KtY',
hasDisclaimer: true,
disclaimerKey: 'youtube',
disclaimerText: `Vai alla pagina privacy <a href='/privacy_policy'>policy</a>`
};
return (
<Video youtube={ytVideo} />
);
}
};

export const Esempi: Story = {
render: () => {
const sources = [
{ src: '//vjs.zencdn.net/v/oceans.mp4', type: 'video/mp4' },
{ src: '//vjs.zencdn.net/v/oceans.webm', type: 'video/webm' }
];

return (
<Video sources={sources} />
);
}
};
54 changes: 54 additions & 0 deletions stories/Documentation/Video.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Canvas, Controls, Meta, Story } from '@storybook/blocks';
import { Code } from '@storybook/components';
import { Callout, CalloutText, CalloutTitle } from '../../src';
import * as VideoStories from '../Components/Video.stories';

<Meta of={VideoStories} />

# VIDEO

Il componente Video utilizza la libreria video.js per implementare funzionalità
avanzate come il supporto a diversi formati video, la personalizzazione
dell’interfaccia utente e l’integrazione con API esterne.

Fare riferimento alla [documentazione Bootstrap per il video](https://italia.github.io/bootstrap-italia/docs/componenti/video-player/)

## Esempio base

In questo esempio vengono passati al compomente solamente la prorietà relativa
all'array delle sorgenti, contente per ogni sorgente l'url della sorgente
e sua tipologia.

<Canvas of={VideoStories.Esempi} />

## Trascrizioni

In questo esempio viene passato del testo relativo alla trascrizione video

<Canvas of={VideoStories.Trascription} />

## Tracce

In questo esempio vengono passate diverse tracce per i sottotitoli:
in italiano (predefinito) , inglese e spagnolo

<Canvas of={VideoStories.Tracks} />

## Autoplay e controlli

In questo esempio vengono impostati seguenti parametri:

- autoPlay: true
- fluid: true
- controls: true
- loop: true

<Canvas of={VideoStories.AutoplayAndControls} />

## Video YouTube con accettazione

In questo esempio viene passato nella prorietà `youtubeUrl` un url di un video
di YouTube.

<Canvas of={VideoStories.Esempi} />

2 changes: 1 addition & 1 deletion test/Accordion.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';

import { Accordion, AccordionHeader } from '../src';
import { AccordionItem } from 'reactstrap';

Expand Down
Loading
Loading