Skip to content
Closed
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
19 changes: 18 additions & 1 deletion libs/next_gen_ui_agent/data_transform/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,27 @@ def main_processing(self, data: Any, component: UIComponentMetadata):
):
video_url = str(field_name_like_url.data[0])

# Try to find poster/picture image URL for video_img
if not self._component_data.video_img:
from next_gen_ui_agent.data_transform.types import IMAGE_DATA_PATH_SUFFIXES

poster_field = data_transformer_utils.find_simple_value_field_by_data_path(
fields,
lambda name: name.lower().endswith(IMAGE_DATA_PATH_SUFFIXES),
)
if (
poster_field
and len(poster_field.data) > 0
and is_url_http(str(poster_field.data[0]))
):
self._component_data.video_img = str(poster_field.data[0])

if not video_url:
logger.warning("No video url found in Video Component")
self._component_data.video = None
self._component_data.video_img = None
# Only clear video_img if no poster was found independently
if not self._component_data.video_img:
self._component_data.video_img = None
else:
self._component_data.video = str(video_url)

Expand Down
169 changes: 169 additions & 0 deletions libs_js/next_gen_ui_react/src/components/VideoPlayerWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import {
Card,
CardBody,
CardTitle,
Title,
} from "@patternfly/react-core";
import React from "react";

interface VideoPlayerProps {
id?: string;
title: string;
video?: string | null;
video_img?: string | null;
className?: string;
autoPlay?: boolean;
controls?: boolean;
width?: string | number;
height?: string | number;
}

const VideoPlayerWrapper: React.FC<VideoPlayerProps> = ({
id,
title,
video,
video_img,
className,
autoPlay = false,
controls = true,
width = "100%",
height = "auto",
}) => {
// Extract video ID from YouTube URL for embedding
const getYouTubeEmbedUrl = (url: string) => {
// Match various YouTube URL formats
let videoId = '';

// Standard YouTube URL: https://www.youtube.com/watch?v=VIDEO_ID
const standardMatch = url.match(/[?&]v=([^&\s]{11})/);
if (standardMatch) {
videoId = standardMatch[1];
} else {
// Short YouTube URL: https://youtu.be/VIDEO_ID
const shortMatch = url.match(/youtu\.be\/([^?\s]{11})/);
if (shortMatch) {
videoId = shortMatch[1];
}
}

if (videoId) {
const autoPlayParam = autoPlay ? '&autoplay=1' : '';
return `https://www.youtube.com/embed/${videoId}?rel=0${autoPlayParam}`;
}
return url;
};

// Check if URL is a YouTube video
const isYouTubeUrl = (url: string) => {
return url.includes('youtube.com') || url.includes('youtu.be');
};

const renderVideoContent = () => {
if (video) {
if (isYouTubeUrl(video)) {
// YouTube video embedding
return (
<div
style={{
position: 'relative',
paddingBottom: '56.25%', // 16:9 aspect ratio
height: 0,
overflow: 'hidden',
maxWidth: '100%',
background: '#000',
borderRadius: 'var(--pf-global--BorderRadius--sm)'
}}
>
<iframe
title={title}
src={getYouTubeEmbedUrl(video)}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
border: 0
}}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerPolicy="strict-origin-when-cross-origin"
allowFullScreen
/>
</div>
);
} else {
// Direct video file
return (
<video
src={video}
controls={controls}
autoPlay={autoPlay}
style={{
width,
height,
maxWidth: '100%',
borderRadius: 'var(--pf-global--BorderRadius--sm)'
}}
title={title}
>
Your browser does not support the video tag.
</video>
);
}
} else if (video_img) {
// Show poster image when no video URL is provided
return (
<div style={{ textAlign: 'center' }}>
<img
src={video_img}
alt={title}
style={{
maxWidth: '100%',
height: 'auto',
borderRadius: 'var(--pf-global--BorderRadius--sm)',
objectFit: 'cover',
}}
/>
</div>
);
}

// Fallback when neither video nor poster image is available
return (
<div
style={{
textAlign: 'center',
padding: '2rem',
backgroundColor: 'var(--pf-global--BackgroundColor--200)',
borderRadius: 'var(--pf-global--BorderRadius--sm)',
color: 'var(--pf-global--Color--200)'
}}
>
No video content available
</div>
);
};

return (
<Card
id={id}
className={className}
style={{
maxWidth: "1440px",
margin: "0 auto",
width: "100%"
}}
>
<CardTitle>
<Title headingLevel="h4" size="lg">
{title}
</Title>
</CardTitle>
<CardBody>
{renderVideoContent()}
</CardBody>
</Card>
);
};

export default VideoPlayerWrapper;
2 changes: 2 additions & 0 deletions libs_js/next_gen_ui_react/src/constants/componentsMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import OneCardWrapper from "../components/OneCardWrapper";
import QuickResponse from "../components/QuickResponse";
import TableWrapper from "../components/TableWrapper";
import Text from "../components/Text";
import VideoPlayerWrapper from "../components/VideoPlayerWrapper";

export const componentsMap = {
accordion: AccordionWrapper,
Expand All @@ -33,4 +34,5 @@ export const componentsMap = {
"one-card": OneCardWrapper,
quickResponse: QuickResponse,
table: TableWrapper,
"video-player": VideoPlayerWrapper,
};
Loading