Skip to content

Commit

Permalink
Add basic video framestep controls
Browse files Browse the repository at this point in the history
  • Loading branch information
ugyballoons committed Nov 1, 2024
1 parent a6764e8 commit 6911a42
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 50 deletions.
11 changes: 8 additions & 3 deletions python/lsst/ts/rubintv/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ class HasButton(BaseModel):
text_shadow: bool = False


class MediaType(str, Enum):
IMAGE: str = "image"
VIDEO: str = "video"


class MosaicViewMeta(BaseModel):
"""Populated in the models data YAML file, each MosaicViewMeta object pairs
a channel name with a set of metadata columns to display alongside the
Expand All @@ -83,13 +88,13 @@ class MosaicViewMeta(BaseModel):
The channel name.
metaColumns : list[str]
A list of metadata columns.
dataType : str
Presently, "image" or "video" are only options.
dataType : MediaType
Presently, IMAGE or VIDEO are the only options.
"""

channel: str
metaColumns: list[str]
dataType: str = "image"
mediaType: MediaType = MediaType.IMAGE


class ExtraButton(HasButton):
Expand Down
7 changes: 7 additions & 0 deletions python/lsst/ts/rubintv/models/models_data.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ cameras:
title: Current Image
per_day: True

mosaic_view_meta:
- channel: movies
mediaType: video
metaColumns: []

- name: comcam
title: ComCam
online: True
Expand Down Expand Up @@ -257,8 +262,10 @@ cameras:

mosaic_view_meta:
- channel: day_movie
mediaType: video
metaColumns: []
- channel: last_n_movie
mediaType: video
metaColumns: []

copy_row_template: "dataId = {\"day_obs\": {dayObs}, \"seq_num\": \
Expand Down
199 changes: 153 additions & 46 deletions src/js/components/MosaicView.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,55 @@ import {
metadataType,
mosaicSingleView,
} from "./componentPropTypes"
import { _getById, addStrHashCode } from "../modules/utils"

const FRAMELENGTH = 0.1
const BACK = -FRAMELENGTH
const FORWARD = FRAMELENGTH

// add hashing method to String prototype
addStrHashCode()

const commonColumns = ["seqNum"]

export default function MosaicView({ locationName, camera }) {
const [historicalBusy, setHistoricalBusy] = useState(null)
const [currentMeta, setCurrentMeta] = useState({})
const initialViews = camera.mosaic_view_meta.map((view) => ({
...view,
latestEvent: {},
}))
const isFocusable = checkNeedsFocusability()
const [views, setViews] = useState(initialViews)

function initialViews() {
const views = camera.mosaic_view_meta.map((view, index) => ({
...view,
latestEvent: {},
hasFocus: index == 0 ? true : false,
}))
return views
}

function checkNeedsFocusability() {
// Is there more than one video?
const vids = camera.mosaic_view_meta.filter(
({ mediaType }) => mediaType === "image"
)
return vids.length > 1 ? true : false
}

function setHasFocus(thisView) {
setViews((prevViews) =>
prevViews.map((view) =>
view.channel === thisView.channel
? { ...view, hasFocus: true }
: { ...view, hasFocus: false }
)
)
}

useEffect(() => {
window.addEventListener("camera", handleMetadataChange)
window.addEventListener("historicalStatus", handleHistoricalStateChange)
window.addEventListener("channel", handleChannelEvent)

function handleMetadataChange(event) {
const { data, dataType } = event.detail
if (Object.entries(data).length < 1 || dataType !== "metadata") {
Expand Down Expand Up @@ -58,21 +89,22 @@ export default function MosaicView({ locationName, camera }) {
)
window.removeEventListener("channel", handleChannelEvent)
}
})
}, [])

return (
<div className="viewsArea">
<ul className="views">
{views.map((view) => {
return (
<li key={view.channel} className="view">
<ChannelView
locationName={locationName}
camera={camera}
view={view}
currentMeta={currentMeta}
/>
</li>
<ChannelView
locationName={locationName}
camera={camera}
view={view}
currentMeta={currentMeta}
setHasFocus={setHasFocus}
isFocusable={isFocusable}
key={view.channel}
/>
)
})}
</ul>
Expand All @@ -84,59 +116,75 @@ MosaicView.propTypes = {
camera: cameraType,
}

function ChannelView({ locationName, camera, view, currentMeta }) {
function ChannelView({
locationName,
camera,
view,
currentMeta,
setHasFocus,
isFocusable,
}) {
const channel = camera.channels.find(({ name }) => name === view.channel)
if (!channel) {
return <h3>Channel {view.channel} not found</h3>
}
const { latestEvent: { day_obs: dayObs }} = view
const {
hasFocus,
mediaType,
latestEvent: { day_obs: dayObs },
} = view
const clsName = ["view", `view-${mediaType}`, hasFocus ? "has-focus" : null]
.join(" ")
.trimEnd()
const clickHandler = isFocusable ? () => setHasFocus(view) : null
return (
<>
<h3 className="channel">{channel.title}
{ dayObs && (
<span>: { dayObs }</span>
) }
<li className={clsName} onClick={clickHandler}>
<h3 className="channel">
{channel.title}
{dayObs && <span>: {dayObs}</span>}
</h3>
<ChannelMedia
locationName={locationName}
camera={camera}
event={view.latestEvent}
mediaType={mediaType}
/>
<ChannelMetadata
view={view}
metadata={currentMeta}
/>
</>
<ChannelMetadata view={view} metadata={currentMeta} />
</li>
)
}
ChannelView.propTypes = {
locationName: PropTypes.string,
camera: cameraType,
view: mosaicSingleView,
currentMeta: metadataType
}

function ChannelMedia({ locationName, camera, event }) {
const { filename, ext } = event
const mediaURL = buildMediaURI(locationName, camera.name, event.channel_name, filename)
switch (ext) {
case 'mp4':
return <ChannelVideo mediaURL={mediaURL}/>
case 'jpg':
case 'jpeg':
case 'png':
return <ChannelImage mediaURL={mediaURL}/>
currentMeta: metadataType,
}

function ChannelMedia({ locationName, camera, event, mediaType }) {
const { filename } = event
if (!filename) return <ChannelMediaPlaceholder />
const mediaURL = buildMediaURI(
locationName,
camera.name,
event.channel_name,
filename
)
switch (mediaType) {
case "video":
return <ChannelVideo mediaURL={mediaURL} />
case "image":
default:
return <ChannelMediaPlaceholder/>
return <ChannelImage mediaURL={mediaURL} />
}
}
ChannelMedia.propTypes = {
locationName: PropTypes.string,
camera: cameraType,
event: eventType,
mediaType: PropTypes.string,
}

function ChannelImage({mediaURL}) {
function ChannelImage({ mediaURL }) {
const imgSrc = new URL(`event_image/${mediaURL}`, APP_DATA.baseUrl)
return (
<div className="viewImage">
Expand All @@ -150,15 +198,18 @@ ChannelImage.propTypes = {
mediaURL: PropTypes.string,
}

function ChannelVideo({mediaURL}) {
function ChannelVideo({ mediaURL }) {
const videoSrc = new URL(`event_video/${mediaURL}`, APP_DATA.baseUrl)
const vidID = `v_${mediaURL.hashCode()}`
return (
<div className="viewVideo">
<a href={videoSrc}>
<video className="resp" controls autoPlay loop>
<source src={videoSrc}/>
<video className="resp" id={vidID} autoPlay loop controls>
<source src={videoSrc} />
</video>
</a>
<button onClick={() => frameStep(vidID, BACK)}>&lt;</button>
<button onClick={() => frameStep(vidID, FORWARD)}>&gt;</button>
</div>
)
}
Expand All @@ -175,7 +226,11 @@ function ChannelMediaPlaceholder() {
}

function ChannelMetadata({ view, metadata }) {
const { channel, metaColumns: viewColumns, latestEvent: {seq_num: seqNum} } = view
const {
channel,
metaColumns: viewColumns,
latestEvent: { seq_num: seqNum },
} = view
if (viewColumns.length == 0) {
return
}
Expand All @@ -188,7 +243,9 @@ function ChannelMetadata({ view, metadata }) {
const value = metadatum[column] ?? "No value set"
return (
<tr key={column} className="viewMetaCol">
<th scope="row" className="colName">{column}</th>
<th scope="row" className="colName">
{column}
</th>
<td className="colValue">{value}</td>
</tr>
)
Expand All @@ -204,3 +261,53 @@ ChannelMetadata.propTypes = {

const buildMediaURI = (locationName, cameraName, channelName, filename) =>
`${locationName}/${cameraName}/${channelName}/${filename}`

function frameStep(vidID, timeDelta) {
console.log('frame delta is: ',timeDelta)
const video = _getById(vidID)
pauseVideo(video)
if (timeDelta < 0 && video.currentTime < 0) {
video.currentTime = 0
} else if (timeDelta > 0 && video.currentTime > video.duration) {
video.currentTime = video.duration
} else {
video.currentTime = video.currentTime + timeDelta
}
}

window.onkeydown = videoControl

function videoControl(e) {
const video = document.querySelector(".view-video.has-focus video")
if (!video) {
return
}
const key = e.code
let timeDelta = 0
switch(key) {
case "ArrowLeft":
timeDelta = BACK
break
case "ArrowRight":
timeDelta = FORWARD
break
case "Space":
if (video.isPaused) {
console.log('Gonna play again!')
video.play()
} else {
video.pause()
}
}
if (timeDelta) {
frameStep(video.id, timeDelta)
}
}

function pauseVideo(video) {
if (!video.isPaused) {
video.pause()
return true
}
return false
}
10 changes: 10 additions & 0 deletions src/js/modules/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,13 @@ export function getWebSockURL (name) {
const appName = window.location.pathname.split('/')[1]
return `${wsProtocol}//${hostname}/${appName}/${name}/`
}

export function addStrHashCode () {
String.prototype.hashCode = function() {
var hash = 0, i = 0, len = this.length
while ( i < len ) {
hash = ((hash << 5) - hash + this.charCodeAt(i++)) << 0
}
return hash
}
}
1 change: 0 additions & 1 deletion src/js/pages/mosaic-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { _getById } from "../modules/utils"
ws.subscribe("service", "channel", locationName, camera.name, view.channel)
const channel = camera.channels.find(({name}) => name === view.channel)
if (!channel.per_day) {
console.log(`${channel.name} is sequenced`)
hasSequencedChannels = true
}
})
Expand Down
5 changes: 5 additions & 0 deletions src/sass/style.sass
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,11 @@ section
width: 100%
padding: 1em
background-color: rgba(0, 0, 0, 0.2)
transition: 0.3s box-shadow
// complicated looking way of highlighting only one
// selected video view in a list of video views
:has(.view-video:not(.has-focus)) .has-focus
box-shadow: 0 0 13px rgba(255,255,255,0.3)

.viewImage
width: 100%
Expand Down

0 comments on commit 6911a42

Please sign in to comment.