globalVariables("playstable_add_mp4_col") ## suppress check warning about this
ovva_shiny_server <- function(app_data) {
function(input, output, session) {
trace_execution <- FALSE ## for debugging
debug_mp4 <- FALSE
plays_cols_to_show <- c("home_team", "visiting_team", "video_time", "code", "set_number", "home_team_score", "visiting_team_score")
adfilter_cols_to_show <- c(##"time", "video_time", "code", "team", "player_number",
"Skill rating code" = "evaluation_code", "Skill rating" = "evaluation",
"Attack code" = "attack_code", "Attack code description" = "attack_description",
"Setter call" = "set_code", "Setter call description" = "set_description", ##"set_type",
"Start zone" = "start_zone", "End zone" = "end_zone", #"End subzone" = "end_subzone",
"End cone" = "end_cone",
"Skill subtype" = "skill_subtype", "Number of players" = "num_players", ##"num_players_numeric",
"Special code" = "special_code",
"Home team score" = "home_team_score", "Visiting team score" = "visiting_team_score",
"Home team rotation (setter position)" = "home_setter_position", "Visiting team rotation (setter position)" = "visiting_setter_position",
"Opposition team rotation (setter position)" = "opposition_setter_position",
"Custom code" = "custom_code",
##"attack_phase", ## not really needed with phase already used
##"start_coordinate_x", "start_coordinate_y", "mid_coordinate_x", "mid_coordinate_y", "end_coordinate_x", "end_coordinate_y",
##home_player_id1", "home_player_id2", "home_player_id3", "home_player_id4", "home_player_id5", "home_player_id6",
##"visiting_player_id1", "visiting_player_id2", "visiting_player_id3", "visiting_player_id4", "visiting_player_id5", "visiting_player_id6",
"Set number" = "set_number", ##"team_touch_id",
"Home team" = "home_team", "Visiting team" = "visiting_team", "Point won by" = "point_won_by",
"Receiving team" = "receiving_team", "Serving team" = "serving_team", ##"game_date",
"Receiving team rotation (setter position)" = "receiving_setter_position", "Serving team rotation (setter position)" = "serving_setter_position",
"Breakpoint/sideout" = "breakpoint/sideout", "Rotation (setter position)" = "setter_position",
"Receiving player" = "receiving_player", "Reception grade" = "reception_grade",
"Serve zone" = "pt_serve_zone", "Pass/dig zone" = "ts_pass_zone",
"Setter on court" = "setter_on_court", "Opposition setter on court" = "opposition_setter_on_court", "Opposition team" = "opposition_team")
## some inits
master_playstable_selected_row <- -99L ## non-reactive
is_fresh_playlist <- FALSE
## helper function: get the right function from the playlist handler for a given skill and specific
funs_from_playlist <- function(specific) {
## return a list of functions
app_data$playlist_handler$fun[which(app_data$playlist_handler$specific %in% specific)]
}
funs_from_highlight <- function(specific) {
## return a list of functions
app_data$highlight_handler$fun[which(app_data$highlight_handler$specific %in% specific)]
}
have_done_startup <- reactiveVal(FALSE)
get_data_paths <- reactive({
if (is.function(app_data$data_path)) {
app_data$data_path()
} else {
app_data$data_path
}
})
## highlight the season selector if it's empty
observe({
if (isTRUE(app_data$no_initial_season_selection)) {
if (identical(input$season, "")) evaljs("$('#season_highlight').addClass('reminder');") else evaljs("$('#season_highlight').removeClass('reminder');")
}
})
## update the season choices
season_choices <- reactive(names(get_data_paths()))
observe({
chc <- season_choices()
isolate(sel <- input$season)
if (isTRUE(app_data$no_initial_season_selection)) chc <- c("Choose" = "", chc) ## no initial selection in this case
if (is.null(sel) || !sel %in% chc) sel <- chc[1]
updateSelectInput(session, "season", choices = chc, selected = sel)
})
## play-by-play data for selected season
meta_unfiltered <- reactiveVal(NULL)
pbp <- reactiveVal(NULL)
pbp_augment <- reactiveVal(NULL)
got_no_video <- reactiveVal(0L) ## only used to show message about no matches with video: 0 = ok, 1 = all files missing video, 2 = all files missing video times and/or videos
season_data_type <- reactiveVal("indoor")
empty_video_list <- dplyr::tibble(match_id = character(), filename = character(), video_source = character())
video_list <- reactiveVal(empty_video_list)
## process metadata for selected season matches and update pbp reactiveVal accordingly
meta <- reactive({
if (!is.null(input$season) && input$season %in% season_choices()) {
isolate({
sdigest <- digest::digest(input$season)
if (trace_execution) cat("recalculating meta\n")
showModal(modalDialog(title = "Processing match metadata ...", footer = NULL, "Please wait"))
if (file.exists(file.path(get_data_paths()[[input$season]], "allmeta.rds"))) {
## use allmeta.rds if available
tmp <- readRDS(file.path(get_data_paths()[[input$season]], "allmeta.rds"))
out <- lapply(tmp, function(z) z$meta)
} else {
myfiles <- dir(get_data_paths()[[input$season]], pattern = "\\.(dvw|psvb)$", ignore.case = TRUE, full.names = TRUE)
dvargs <- if ("dv_read_args" %in% app_data) app_data$dv_read_args else list()
dvargs$metadata_only <- TRUE
out <- lapply(myfiles, function(z) if (grepl("psvb$", z, ignore.case = TRUE)) {
pv_read(z)$meta
} else {
dvargs$filename <- z
do.call(dv_read, dvargs)$meta
})
}
## augment the match_id values with input$season, in case there are the same matches in different data sets (seasons)
out <- lapply(out, function(z) { z$match_id <- paste0(sdigest, "|", z$match_id); z })
if (!is.null(app_data$meta_preprocess) && is.function(app_data$meta_preprocess)) {
try(out <- lapply(out, app_data$meta_preprocess))
}
## check for duplicate match IDs - these could have different video files, which is too much hassle to handle
if (any(duplicated(lapply(out, function(z) z$match_id)))) {
output$processing_note <- renderUI(tags$div(class = "alert alert-danger", "There are duplicate match IDs"))
out <- NULL
} else {
output$processing_note <- renderUI(NULL)
}
meta_unfiltered(out)
## if the plays component doesn't have clock times, then we need to get the date from the metadata
game_dates_meta <- bind_rows(lapply(out, function(z) list(match_id = z$match_id, game_date = z$match$date))) %>%
dplyr::filter(!is.na(.data$game_date))
game_dates_meta <- game_dates_meta[!game_dates_meta$match_id %in% game_dates_meta$match_id[duplicated(game_dates_meta$match_id)], ]
## out is a list of metadata objects
## prune out any that don't have video
if (!is.null(out)) out <- Filter(function(z) !is.null(z$video) && nrow(z$video) > 0, out)
if (length(out) < 1) {
got_no_video(1L)
video_list(empty_video_list)
pbp(NULL)
pbp_augment(NULL)
out <- NULL
season_data_type("indoor") ## default
} else {
season_data_type(tryCatch(if (grepl("beach", out[[1]]$match$regulation)) "beach" else "indoor", error = function(e) "indoor"))
## for each video file, check if it exists and try and find it if not
for (z in seq_along(out)) {
if (is_youtube_id(out[[z]]$video$file)) {
## do nothing
} else if (grepl("^https?://", out[[z]]$video$file, ignore.case = TRUE)) {
## check that the URL is valid, otherwise do nothing
## skip for now, it will be slow with many files
## chk <- tryCatch(httr::status_code(httr::HEAD(out[[z]]$video$file)), error = function(e) 500)
## if (chk >= 300) out[[z]]$video$file <- NA_character_
} else {
try({
if (isTRUE(app_data$video_subtree_only)) {
out[[z]]$video$file <- find_video_in_subtree(dvw_filename = out[[z]]$filename, video_filename = fs::fs_path(out[[z]]$video$file), subtree_only = TRUE, alt_path = app_data$alt_video_path)
} else {
temp <- find_video_in_subtree(dvw_filename = out[[z]]$filename, video_filename = fs::fs_path(out[[z]]$video$file), subtree_only = FALSE, alt_path = app_data$alt_video_path)
out[[z]]$video$file <- ifelse(!fs::file_exists(as.character(out[[z]]$video$file)) && !is.na(temp), temp, out[[z]]$video$file)
}
})
}
}
## keep track of videos
video_list(bind_rows(lapply(out, function(z) list(match_id = z$match_id, filename = z$filename, video_source = if (nrow(z$video) == 1 && !is.na(z$video$file) && nzchar(z$video$file)) z$video$file else NA_character_))))
## remove any files with no associated video
out <- Filter(Negate(is.null), lapply(out, function(z) if (nrow(z$video) == 1 && !is.na(z$video$file) && nzchar(z$video$file)) z))
if (length(out) < 1) {
## no files with video
got_no_video(1L)
pbp(NULL)
pbp_augment(NULL)
out <- NULL
} else {
## will also need to check actual plays data to remove any files with all-missing video times
showModal(modalDialog(title = "Processing match data ...", footer = NULL, "Please wait"))
if (file.exists(file.path(get_data_paths()[[input$season]], "alldata.rds"))) {
## use alldata.rds if available
mydat <- readRDS(file.path(get_data_paths()[[input$season]], "alldata.rds"))
} else {
myfiles <- dir(get_data_paths()[[input$season]], pattern = "\\.(dvw|psvb)$", ignore.case = TRUE, full.names = TRUE)
dvargs <- if ("dv_read_args" %in% app_data) app_data$dv_read_args else list()
if (!"skill_evaluation_decode" %in% names(dvargs)) dvargs$skill_evaluation_decode <- "guess"
mydat <- bind_rows(lapply(myfiles, function(z) if (grepl("psvb$", z)) {
pv_read(z)$plays
} else {
dvargs$filename <- z
do.call(dv_read, dvargs)$plays
}))
}
## augment the match_id values with input$season to match what we did to the match_ids in the metadata above
mydat$match_id <- paste0(sdigest, "|", mydat$match_id)
## check for all-missing video times now
no_video_times_mids <- mydat %>% group_by(.data$match_id) %>% dplyr::summarize(nv = all(is.na(.data$video_time))) %>% dplyr::filter(.data$nv) %>% dplyr::pull(.data$match_id)
## remove those from out
out <- Filter(Negate(is.null), lapply(out, function(z) if (z$match_id %in% no_video_times_mids) NULL else z))
if (length(out) < 1) {
## no files with video/video times
got_no_video(2L)
pbp(NULL)
pbp_augment(NULL)
out <- NULL
} else {
my_match_ids <- as.character(lapply(out, function(z) z$match_id))
mydat <- dplyr::filter(mydat, .data$match_id %in% my_match_ids)
got_no_video(0L)
## now process pbp()
mydat <- mydat[mydat$match_id %in% my_match_ids, ]
mydat <- ungroup(mutate(group_by(mydat, .data$match_id), game_date = if (all(is.na(.data$time))) as.Date(NA) else min(as.Date(.data$time), na.rm = TRUE)))
## replace missing game dates with those from meta, if we can
mydat <- left_join(mydat, game_dates_meta %>% dplyr::rename(game_date2 = "game_date"), by = "match_id") %>%
mutate(game_date = if_else(is.na(.data$game_date) | is.infinite(.data$game_date), .data$game_date2, .data$game_date)) %>%
dplyr::select(-"game_date2")
## mydat <- mutate(mydat, game_id = paste0(gsub('\\b(\\pL)\\pL{1,}|.','\\U\\1', .data$home_team, perl = TRUE),
## "_", gsub('\\b(\\pL)\\pL{1,}|.','\\U\\1',.data$visiting_team, perl = TRUE)))
## mydat <- mutate(mydat, game_id = case_when(!is.na(.data$game_date) & !is.infinite(.data$game_date) ~ paste0(.data$game_date, "_", .data$game_id),
## TRUE ~ .data$game_id))
## ## de-duplicate game_ids
## dedup <- mutate(distinct(dplyr::select(mydat, .data$match_id, .data$game_id)), game_id = make.unique(.data$game_id, sep = "_"))
## mydat <- left_join(dplyr::select(mydat, -"game_id"), dedup, by = "match_id")
pbp(mydat)
## Augment pbp with additional covariates
pbp_augment(preprocess_data(mydat, data_type = season_data_type()))
}
}
}
removeModal()
have_done_startup(TRUE)
})
out
} else {
isolate({
got_no_video(0L)
meta_unfiltered(NULL)
video_list(empty_video_list)
pbp(NULL)
pbp_augment(NULL)
season_data_type("indoor") ## default
})
NULL
}
})
## Games
last_game_table_hash <- ""
game_table_dropdown <- reactiveVal(NULL)
observe({
if (trace_execution) cat("updating game_table_dropdown()\n")
out <- if (is.null(pbp_augment()) || nrow(pbp_augment()) < 1) {
isolate({
## don't be reactive to these, just use them to tailor the message
output$no_game_data <- renderUI(
if (is.null(input$season)) {
tags$div(class = "alert alert-info", "No competition data sets. Log in?")
} else if (!is.null(meta()) && is.null(pbp())) {
tags$div(class = "alert alert-danger", "All matches are missing their video files.")
} else if (is.null(meta()) && have_done_startup()) {
if (isolate(got_no_video()) > 1L) {
tags$div(class = "alert alert-danger", "All matches are missing their video files and/or have not been synchronized with video.")
} else if (isolate(got_no_video()) > 0L) {
tags$div(class = "alert alert-danger", "All matches are missing their video files.")
} else {
## some other failure
tags$div(class = "alert alert-danger", "Sorry, something went wrong processing this data set.")
}
} else {
NULL
})
})
## hide the game selector if we have no games with video (or haven't selected a data set)
js_hide("game_table_dropdown")
character()
} else {
output$no_game_data <- renderUI(NULL)
js_show("game_table_dropdown")
## Customize pbp
datatble <- distinct(pbp_augment(), .data$match_id, .data$game_date, .data$visiting_team, .data$home_team, .keep_all = FALSE)
if (all(is.na(datatble$game_date))) {
datatble <- dplyr::mutate(datatble, display_ID = paste0("Unknown date: ",.data$home_team," - ",.data$visiting_team))
} else {
datatble <- dplyr::mutate(datatble, display_ID = paste0(ifelse(is.na(.data$game_date), "Unknown date", format(.data$game_date, "%d %b %Y")),": ",.data$home_team," - ",.data$visiting_team))
}
datatble <- dplyr::arrange(datatble, .data$game_date)
## named list, so that display_ID gets shown but the code can operate directly on match_id as the selected value
setNames(as.list(datatble$match_id), datatble$display_ID)
}
gt_hash <- digest::digest(out)
if (last_game_table_hash != gt_hash) {
last_game_table_hash <<- gt_hash
game_table_dropdown(out)
if (trace_execution) cat("updating input$game_table_dropdown\n")
sel <- intersect(out, input$game_table_dropdown)
updatePickerInput(session, "game_table_dropdown", choices = out, selected = sel)
}
})
selected_match_id <- reactive({
if (trace_execution) cat("updating selected_match_id\n")
unique(na.omit(input$game_table_dropdown))
})
## Team
team_list <- reactiveVal(NULL)
observe({
if (trace_execution) cat("recalculating team_list\n")
blah <- selected_match_id() ## reactive to this
pbp <- pbp_augment() ## no reason to isolate()?
tl <- if (is.null(pbp) || nrow(pbp) < 1) {
character()
} else {
tmp <- dplyr::filter(pbp, .data$match_id %in% selected_match_id())
sort(unique(na.omit(tmp$team)))
}
team_list(tl)
isolate(sel <- intersect(tl, input$team_list))
if (length(sel) < 1) sel <- character()## ## select none, which will be treated as "no filter" ## previously tl ## select all
updatePickerInput(session, "team_list", choices = tl, selected = sel)
})
## Player ID
player_list <- reactiveVal(NULL)
last_player_list_hash <- ""
observe({
if (trace_execution) cat("recalculating player_list\n")
blah <- list(selected_match_id(), input$team_list) ## reactive to these and pbp_augment
pl <- if (is.null(pbp_augment()) || nrow(pbp_augment()) < 1) {
character()
} else {
tmp <- dplyr::filter(pbp_augment(), .data$match_id %in% selected_match_id(), all_or_filter(.data$team, input$team_list))
sort(unique(na.omit(tmp$player_name)))
}
pl_hash <- digest::digest(pl)
if (last_player_list_hash != pl_hash) {
if (trace_execution) cat(" updating player selector\n")
last_player_list_hash <<- pl_hash
player_list(pl)
isolate(sel <- intersect(pl, input$player_list))
if (length(sel) < 1) sel <- character() ## select none, which will be treated as "no filter" ## previously pl ## select all
updatePickerInput(session, "player_list", choices = pl, selected = sel)
} else {
if (trace_execution) cat(" player list unchanged\n")
}
})##, priority = -100)
## Skill
skill_list <- reactiveVal(NULL)
last_skill_list_hash <- ""
observe({
if (trace_execution) cat("recalculating skill_list\n")
sl <- if (is.null(pbp_augment()) || nrow(pbp_augment()) < 1) {
character()
} else {
tmp <- dplyr::filter(pbp_augment(), .data$match_id %in% selected_match_id(), all_or_filter(.data$player_name, input$player_list), all_or_filter(.data$team, input$team_list))
sort(unique(na.omit(tmp$skill)))
}
sl_hash <- digest::digest(sl)
if (last_skill_list_hash != sl_hash) {
if (trace_execution) cat(" updating skill selector\n")
last_skill_list_hash <<- sl_hash
skill_list(sl)
## isolate(cat("+SS:\n", str(input$skill_list), "\n-SS\n"))
isolate(sel <- intersect(sl, input$skill_list))
if (length(sel) < 1) sel <- character() ## select none, which will be treated as "no filter" ## previously sl ## select all
updatePickerInput(session, "skill_list", choices = sl, selected = sel)
} else {
if (trace_execution) cat(" skill list unchanged\n")
}
})##, priority = -101)
## Pre-defined playlist
playlist_list <- reactive({
tryCatch(
app_data$playlist_handler$specific[all_or_filter(app_data$playlist_handler$skill, input$skill_list)],
error = function(e) character())
})
output$playlist_based_ui <- renderUI({
if (length(playlist_list()) < 1) {
if (length(skill_list()) < 1) {
tags$div(class = "alert alert-info", "Choose a skill first")
} else {
tags$div(class = "alert alert-info", "No playlists have been defined for the chosen skill")
}
} else {
## populate playlist_list options, keeping any existing selections
isolate(sel <- input$playlist_list)
if (!is.null(sel)) sel <- intersect(sel, playlist_list())
pickerInput(inputId = "playlist_list",
label = "Playlists",
choices = playlist_list(),
selected = sel,
options = list(`actions-box` = TRUE),
multiple = TRUE)
}
})
## Highlights
highlight_list <- reactive({
c("None", app_data$highlight_handler$specific[app_data$highlight$skill %in% "Highlights"])
})
output$highlight_based_ui <- renderUI({
if (length(selected_match_id()) < 1) {
tags$div(class = "alert alert-info", "Choose a game first")
} else {
## populate highlight_list options, keeping any existing selections
shiny::isolate(sel <- input$highlight_list)
if (!is.null(sel)) sel <- intersect(sel, highlight_list())
pickerInput(inputId = "highlight_list",
label = "Highlights",
choices = highlight_list(),
selected = sel,
options = list(`actions-box` = TRUE),
multiple = FALSE)
}
})
## Skilltype
skilltype_list <- reactive({
if (is.null(pbp_augment()) || nrow(pbp_augment()) < 1) {
character()
} else {
tmp <- dplyr::filter(pbp_augment(), .data$match_id %in% selected_match_id(), all_or_filter(.data$player_name, input$player_list), all_or_filter(.data$skill, input$skill_list), all_or_filter(.data$team, input$team_list))
sort(unique(tmp$skilltype))
}
})
observe({
## if the skilltype list changes, then we need to select all, otherwise we may have changes from e.g. just attacks to all skills, but we'll be restricted to just the previously-selected attack skill types
updatePickerInput(session, "skilltype_list", choices = skilltype_list(), selected = skilltype_list())
})
## Phase
phase_list <- reactive({
if (is.null(pbp_augment()) || nrow(pbp_augment()) < 1) {
character()
} else {
tmp <- dplyr::filter(pbp_augment(), .data$match_id %in% selected_match_id(), all_or_filter(.data$player_name, input$player_list), all_or_filter(.data$team, input$team_list), all_or_filter(.data$skill, input$skill_list))
sort(unique(tmp$phase))
}
})
observe({
isolate(sel <- intersect(phase_list(), input$phase_list))
if (length(sel) < 1) sel <- phase_list() ## select all
updatePickerInput(session, "phase_list", choices = phase_list(), selected = sel)
})
## Advanced filter
adFilter_list <- reactive({
if (is.null(pbp_augment()) || nrow(pbp_augment()) < 1) {
character()
} else {
temp <- dplyr::filter(pbp_augment(), .data$match_id %in% selected_match_id(), all_or_filter(.data$player_name, input$player_list), all_or_filter(.data$team, input$team_list), all_or_filter(.data$skill, input$skill_list))
avail <- colnames(temp)
avail <- avail[vapply(avail, function(z) !all(is.na(temp[[z]])), FUN.VALUE = TRUE)] ## exclude all-NA cols
avail <- adfilter_cols_to_show[adfilter_cols_to_show %in% avail] ## only those in our pre-defined list of adfilter_cols_to_show
## also refine by data_type
if (grepl("beach", season_data_type())) {
avail <- avail[!avail %in% c("set_code", "set_description", "home_setter_position", "visiting_setter_position", "opposition_setter_position", "receiving_setter_position", "serving_setter_position", "setter_position", "setter_on_court", "opposition_setter_on_court")]
}
avail <- avail[order(names(avail))]
c(list("No filter" = ""), avail) ## add a "no filter" option
}
})
observe({
isolate(sel <- intersect(adFilter_list(), input$adFilter_list))
if (length(sel) < 1) sel <- character() ## select none
updateSelectInput(session, "adFilter_list", choices = adFilter_list(), selected = sel)
})
## Advanced filter value
adFilterValue_list <- reactive({
col_to_select <- input$adFilter_list
if (is.null(col_to_select) || !nzchar(col_to_select)) return(list())
col_to_select <- col_to_select[nzchar(col_to_select)]
if (is.null(pbp_augment()) || nrow(pbp_augment()) < 1 || length(col_to_select) < 1) {
character()
} else {
tmp <- dplyr::filter(pbp_augment(), .data$match_id %in% selected_match_id(), all_or_filter(.data$player_name, input$player_list), all_or_filter(.data$team, input$team_list), all_or_filter(.data$skill, input$skill_list))
sort(unique(tmp[[col_to_select]]))
}
})
observe({
isolate(sel <- intersect(adFilterValue_list(), input$adFilterValue_list))
if (length(sel) < 1) sel <- adFilterValue_list() ## select all
updatePickerInput(session, "adFilterValue_list", choices = adFilterValue_list(), selected = sel)
})
## Advanced filter 2
observe({
isolate(sel <- intersect(adFilter_list(), input$adFilterB_list))
if (length(sel) < 1) sel <- character() ## select none
updateSelectInput(session, "adFilterB_list", choices = adFilter_list(), selected = sel)
})
## Advanced filter 2 value
adFilterBValue_list <- reactive({
col_to_select <- input$adFilterB_list
if (is.null(col_to_select) || !nzchar(col_to_select)) return(list())
col_to_select <- col_to_select[nzchar(col_to_select)]
if (is.null(pbp_augment()) || nrow(pbp_augment()) < 1 || length(col_to_select) < 1) {
character()
} else {
tmp <- dplyr::filter(pbp_augment(), .data$match_id %in% selected_match_id(), all_or_filter(.data$player_name, input$player_list), all_or_filter(.data$team, input$team_list), all_or_filter(.data$skill, input$skill_list))
sort(unique(tmp[[col_to_select]]))
}
})
observe({
isolate(sel <- intersect(adFilterBValue_list(), input$adFilterBValue_list))
if (length(sel) < 1) sel <- adFilterBValue_list() ## select all
updatePickerInput(session, "adFilterBValue_list", choices = adFilterBValue_list(), selected = sel)
})
## Help
observeEvent(input$help, rintrojs::introjs(session, options = list("nextLabel" = "Next", "prevLabel" = "Previous", "skipLabel" = "Skip")))
## which vars are available for custom sort?
observe({
##sortchc <- c()
can_sort_on <- function(z) !is.null(z) && (length(z) < 1 || length(z) > 1)
## some inputs can now be NULL (none selected) meaning no filter applied
## so assume that the fixed ones (match, team, player, skill, skill type, phase) can always be sorted on?
sortchc <- c(Game = "match_id", Team = "team", Player = "player_name", Skill = "skill", `Skill type` = "skilltype", `Phase` = "phase")
## if (can_sort_on(selected_match_id())) sortchc <- c(sortchc, Game = "match_id")
## if (can_sort_on(input$team_list)) sortchc <- c(sortchc, Team = "team")
## if (can_sort_on(input$player_list)) sortchc <- c(sortchc, Player = "player_name")
## if (can_sort_on(input$skill_list)) sortchc <- c(sortchc, Skill = "skill")
## if (can_sort_on(input$skilltype_list)) sortchc <- c(sortchc, `Skill type` = "skilltype")
## if (can_sort_on(input$phase_list)) sortchc <- c(sortchc, `Phase` = "phase")
if (can_sort_on(input$adFilter_list)) {
adfvar <- input$adFilter_list
adfvarname <- names(intersect(adFilter_list(), adfvar))
if (length(adfvarname) != 1) adfvarname <- adfvar
sortchc <- c(sortchc, setNames(adfvar, adfvarname))
}
if (can_sort_on(input$adFilterB_list)) {
adfvar <- input$adFilterB_list
adfvarname <- names(intersect(adFilter_list(), adfvar))
if (length(adfvarname) != 1) adfvarname <- adfvar
sortchc <- c(sortchc, setNames(adfvar, adfvarname))
}
isolate(sel <- intersect(sortchc, input$playlist_sort))
updateSelectInput(session, "playlist_sort", choices = sortchc, selected = sel)
})
playstable_to_delete <- NULL ## vector of logical
playstable_ticked <- NULL ## vector of logical
playstable_display_order <- NULL ## vector of integers
## these three variables are vectors with length equal to number of rows in playstable_data_raw(), and indexed according to playstable_data_raw() row ordering. The actual playstable_data() is playstable_data_raw() but ordered according to playstable_display_order and with user-deleted rows removed
## the raw playstable data (i.e. before reordering or deleting any rows)
playstable_data_raw <- debounce(reactive({
## Customize pbp
if (is.null(pbp_augment()) || nrow(pbp_augment()) < 1 || is.null(selected_match_id()) || is.null(meta())) {
playstable_to_delete <<- NULL
playstable_ticked <<- NULL
playstable_display_order <<- NULL
NULL
} else {
if (trace_execution) cat("recalculating playstable_data\n")
pbp <- pbp_augment()
meta <- meta()
filter_var <- input$adFilter_list
filterB_var <- input$adFilterB_list
## skill, player, team inputs can be NULL (treated as "no filter applied")
was_playlist <- !is.null(input$playlist_list) && !is.null(selected_match_id()) ##&& !is.null(input$skill_list) && !is.null(input$player_list) && !is.null(input$team_list)
was_highlight <- !is.null(input$highlight_list) && !input$highlight_list %eq% "None" && !is.null(selected_match_id())
if (was_playlist || was_highlight) {
myfuns <- if (was_playlist) funs_from_playlist(input$playlist_list) else funs_from_highlight(input$highlight_list)
if (length(selected_match_id()) == 1) {
## apply each of myfuns in turn and rbind the results
pbp_tmp <- bind_rows(lapply(myfuns, function(myfun) myfun(x = dplyr::filter(pbp, .data$match_id %in% selected_match_id()), team = input$team_list, player = input$player_list)))
} else{
pbp_tmp <- dplyr::filter(pbp, .data$match_id %in% selected_match_id())
pbp_tmp <- bind_rows(lapply(myfuns, function(myfun) bind_rows(lapply(split(pbp_tmp, pbp_tmp$match_id), myfun, team = input$team_list, player = input$player_list))))
}
} else {
pbp_tmp <- dplyr::filter(pbp, all_or_filter(.data$player_name, input$player_list), all_or_filter(.data$skill, input$skill_list), all_or_filter(.data$team, input$team_list), .data$match_id %in% selected_match_id())
if (!is.null(input$skilltype_list)) pbp_tmp <- dplyr::filter(pbp_tmp, .data$skilltype %in% input$skilltype_list)
if (!is.null(input$phase_list)) pbp_tmp <- dplyr::filter(pbp_tmp, .data$phase %in% input$phase_list)
}
## advanced filters apply to all
if (!is.null(filter_var) && nzchar(filter_var)) pbp_tmp <- dplyr::filter(pbp_tmp, .data[[filter_var]] %in% input$adFilterValue_list)
if (!is.null(filterB_var) && nzchar(filterB_var)) pbp_tmp <- dplyr::filter(pbp_tmp, .data[[filterB_var]] %in% input$adFilterBValue_list)
playstable_to_delete <<- rep(FALSE, nrow(pbp_tmp))
playstable_ticked <<- rep(FALSE, nrow(pbp_tmp))
playstable_display_order <<- seq_nrows(pbp_tmp)
pbp_tmp$ROWID <- seq_nrows(pbp_tmp) ## keep track of original row numbers for deletion
master_playstable_selected_row <<- 1 ## fresh table/playlist, start from row 1
is_fresh_playlist <<- TRUE
## set playstable_display_order according to chosen vars but leave pbp_tmp with its default ordering and leave this block unreactive to input$playlist_sort
playstable_display_order <<- calc_playstable_order(pbp_tmp)
pbp_tmp
}
}), 250)
## helper function to generate the ordering of the playstable, using input$playlist_sort if it has been set
calc_playstable_order <- function(pl) {
if (missing(pl)) pl <- isolate(playstable_data_raw())
if (is.null(pl)) return(NULL)
pls <- isolate(input$playlist_sort)
if (length(pls) < 1) return(seq_nrows(pl)) ## default ordering
## otherwise according to input$playlist_sort
temp <- intersect(pls, names(pl))
pl %>% ungroup %>% mutate(rownum = seq_len(n())) %>% dplyr::arrange(across(all_of(temp))) %>% dplyr::pull(.data$rownum)
}
## adjust playstable_display_order if input$playlist_sort changes
observe({
blah <- list(input$playlist_sort) ## react to this
temp <- calc_playstable_order()
if (!identical(temp, playstable_display_order)) {
playstable_display_order <<- temp
pl_check_trigger(pl_check_trigger() + 1L)
}
})
## the actual playstable_data is playstable_data_raw but ordered and with user-deleted rows removed
pl_check_trigger <- reactiveVal(0)
playstable_data_bouncy <- reactiveVal(NULL)
playstable_data <- debounce(playstable_data_bouncy, 500)
last_pt_hash <- ""
observe({
if (trace_execution) cat("checking playstable_data\n")
if (!is.null(playstable_data_raw())) {
blah <- pl_check_trigger() ## react to this
ptdel <- playstable_to_delete
ord <- if (length(playstable_display_order) == nrow(playstable_data_raw())) playstable_display_order else seq_nrows(playstable_data_raw())
ord <- ord[!ord %in% which(ptdel)]
out <- playstable_data_raw()[ord, ]
pt_hash <- digest::digest(out)
if (last_pt_hash != pt_hash) {
if (trace_execution) cat(" updating playstable_data\n")
last_pt_hash <<- pt_hash
playstable_data_bouncy(out)
} else {
if (trace_execution) cat(" playstable_data has not changed\n")
}
}
})
observeEvent(input$randomize_playlist, {
if (length(playstable_display_order) > 0) {
playstable_display_order <<- sample.int(length(playstable_display_order), size = length(playstable_display_order), replace = FALSE)
is_fresh_playlist <<- TRUE ## this forces the playlist to re-start playing from the first item
pl_check_trigger(pl_check_trigger() + 1L)
}
})
observeEvent(input$delete_ticked, {
if (isTRUE(any(playstable_ticked[!playstable_to_delete]))) {
plchk <- playstable_to_delete
plchk[which(playstable_ticked)] <- TRUE
playstable_to_delete <<- plchk
is_fresh_playlist <<- FALSE
pl_check_trigger(pl_check_trigger() + 1L)
}
})
observeEvent(input$keep_ticked, {
if (isTRUE(any(playstable_ticked[!playstable_to_delete]))) {
plchk <- playstable_to_delete
## anything already deleted remains deleted
## anything not ticked gets added to the delete list
plchk[which(playstable_to_delete | !playstable_ticked)] <- TRUE
playstable_to_delete <<- plchk
is_fresh_playlist <<- FALSE
pl_check_trigger(pl_check_trigger() + 1L)
}
})
observeEvent(input$reset_ticked, {
## replace any deleted items and revert to the original ordering
playstable_ticked <<- rep(FALSE, length(playstable_ticked))
playstable_to_delete <<- rep(FALSE, length(playstable_to_delete))
playstable_display_order <<- calc_playstable_order()
is_fresh_playlist <<- FALSE
pl_check_trigger(pl_check_trigger() + 1L)
})
observeEvent(input$toggle_plitem, {
## toggle selection of the playlist item
if (!is.null(input$toggle_plitem) && nzchar(input$toggle_plitem) && grepl("@", input$toggle_plitem) && !is.null(playstable_ticked)) {
plchk <- playstable_ticked
thisid <- strsplit(input$toggle_plitem, "@")[[1]][1]
totoggle <- as.numeric(sub("^pl_", "", thisid))
## totoggle will be the row number in the original data, i.e. playstable_data()$ROWID
##cat("toggling:\n "); print(totoggle)
plchk[totoggle] <- !plchk[totoggle]
playstable_ticked <<- plchk
##cat("ticked:\n "); print(which(playstable_ticked))
}
})
output$playstable <- DT::renderDataTable({
mydat <- playstable_data()
scrolly <- if (is.numeric(vo_height())) max(200, vo_height() - 80) else 200 ## 80px for table header row
if (!is.null(mydat)) {
## we are potentially showing a subset of rows of playstable_data_raw() according to playstable_to_delete
tbc <- ifelse(playstable_ticked[mydat$ROWID], "checked", "") ## or subset by playstable_to_delete
mydat$tickboxes <- as.list(paste0('<input type="checkbox" id="pl_', mydat$ROWID, '" onmousedown="event.stopPropagation();" onclick="toggle_pl_item(this);" ', tbc, '/>'))
show_mp4_col <- isTRUE(app_data$mp4_clip_convert) || tryCatch(isTRUE(exists("playstable_add_mp4_col") && is.function(playstable_add_mp4_col) && isTRUE(playstable_add_mp4_col())), error = function(e) FALSE)
if (debug_mp4) cat("show_mp4_col is: ", capture.output(str(show_mp4_col)), "\n")
if (show_mp4_col) {
## don't show mp4 icon on youtube/twitch sources
## video type is not in the playlist yet, this happens when the playlist is built, so do a workaround
isolate(vsrc <- if (!is.null(video_meta()) && nrow(video_meta()) > 0) tryCatch(dplyr::pull(left_join(dplyr::select(mydat, "match_id"), distinct(dplyr::select(video_meta(), "match_id", "video_src")), by = "match_id"), .data$video_src), error = function(e) NA_character_))
if (debug_mp4) { cat("vsrc (0):\n"); print(table(vsrc, useNA = "always")) }
if (length(vsrc) != nrow(mydat)) vsrc <- rep(NA_character_, nrow(mydat))
if (debug_mp4) { cat("vsrc (1):\n"); print(table(vsrc, useNA = "always")) }
vsrc <- case_when(is_youtube_id(vsrc) | grepl("https?://.*youtube", vsrc, ignore.case = TRUE) | grepl("https?://youtu\\.be", vsrc, ignore.case = TRUE) ~ "youtube",
is_twitch_video(vsrc) ~ "twitch",
is.na(vsrc) ~ "unknown",
TRUE ~ "local")
if (debug_mp4) { cat("vsrc (2):\n"); print(table(vsrc, useNA = "always")) }
mydat$mp4 <- as.list(ifelse(vsrc %in% c("local"), paste0('<i class="fa fa-file" id="plmp4_', mydat$ROWID, '" onclick="mp4_pl_item(this);" />'), ""))
}
mydat <- mydat[, c("tickboxes", if (show_mp4_col) "mp4", plays_cols_to_show), drop = FALSE]
cnames <- var2fc(names(mydat))
cnames[1] <- "" ## no name on the delete column
if (show_mp4_col) cnames[2] <- "" ## ditto mp4 if present
js_show("dk_buts")
if (trace_execution) cat("redrawing playstable, master_selected is: ", master_playstable_selected_row, "\n")
## when the table is redrawn but the selected row is not in the first few rows, need to scroll the table - use initComplete callback
DT::datatable(mydat, rownames = FALSE, colnames = cnames, escape = FALSE,
extensions = "Scroller", selection = list(mode = "single", selected = max(master_playstable_selected_row, 1L), target = "row"),
options = list(sDom = '<"top">t<"bottom">rlp', deferRender = TRUE, scrollX = "100%", scrollY = scrolly, scroller = TRUE, ordering = FALSE,
initComplete = DT::JS('function(setting, json) { Shiny.setInputValue("scroll_trigger", new Date().getTime()); }')))
} else {
js_hide("dk_buts")
NULL
}
})
playstable_proxy <- DT::dataTableProxy("playstable", deferUntilFlush = TRUE)
playstable_select_row <- function(rw) {
if (!is.null(rw) && !is.na(rw) && (rw != master_playstable_selected_row)) {
master_playstable_selected_row <<- rw
DT::selectRows(playstable_proxy, rw)
scroll_playstable(rw)
}
}
observeEvent(input$scroll_trigger, scroll_playstable())
scroll_playstable <- function(rw = NULL) {
selr <- if (!is.null(rw)) rw else input$playstable_rows_selected
if (!is.null(selr)) {
## scrolling works on the VISIBLE row index, so it depends on any column filters that might have been applied
visible_rowidx <- which(input$playstable_rows_all == selr)
scrollto <- max(visible_rowidx-1-2, 0) ## -1 for zero indexing, -2 to keep the selected row 2 from the top
evaljs(paste0("$('#playstable').find('.dataTable').DataTable().scroller.toPosition(", scrollto, ", false);"))
}
}
## when player changes item, it triggers input$playstable_current_item via the video_onstart() function. Update the selected row in the playstable
observeEvent(input$playstable_current_item, {
if (!is.null(input$playstable_current_item) && !is.null(playlist())) {
try({
## input$playstable_current_item is 0-based
isolate(np <- nrow(playlist()))
if (np < 1) {
## empty table
master_playstable_selected_row <<- -99L
} else if (input$playstable_current_item < np) {
playstable_select_row(input$playstable_current_item+1)
} else {
## reached the end of the playlist
master_playstable_selected_row <<- -99L
}
})
}
})
## when the user chooses a row in the playstable, it will be selected by that click action, so we just need to play it
## use input$playstable_cell_clicked rather than input$playstable_rows_selected to detect user input, because the latter is also triggered by the player incrementing rows
observeEvent(input$playstable_cell_clicked, { ## note, can't click the same row twice in a row ...
clicked_row <- input$playstable_cell_clicked$row ## 1-based
if (!is.null(clicked_row) && !is.na(clicked_row)) {
if (isTRUE(input$playstable_cell_clicked$col > 0)) {
master_playstable_selected_row <<- clicked_row
evaljs(paste0("dvpl.video_controller.current=", clicked_row-1, "; dvpl.video_play();"))
} else {
## deleted a row
## if it was on or before the current selected row, then subtract one off the master_playstable_selected_row to keep it in sync
if (clicked_row <= master_playstable_selected_row) {
master_playstable_selected_row <<- max(master_playstable_selected_row - 1, 1) ## R 1-based indexing
}
evaljs(paste0("dvpl.video_controller.current=", master_playstable_selected_row-1, ";")) ## -1 for js 0-based indexing
}
}
})
video_meta <- reactive({
if (is.null(pbp_augment()) || nrow(pbp_augment()) < 1 || is.null(meta()) || is.null(selected_match_id())) {
NULL
} else {
if (trace_execution) cat("recalculating video_meta\n")
meta_video <- bind_rows(lapply(meta(), function(z) if (!is.null(z$video)) mutate(z$video, camera = as.character(.data$camera), file = as.character(.data$file), match_id = z$match_id, dvw_filename = z$filename)))
meta_video <- dplyr::filter(meta_video, .data$match_id %in% selected_match_id())
if (nrow(meta_video) < 1) return(NULL)
if (is.string(app_data$video_serve_method) && app_data$video_serve_method %in% c("lighttpd", "servr")) {
## we are serving the video through the lighttpd/servr server, so need to make symlinks in its document root directory pointing to the actual video files
is_url <- is_youtube_id(meta_video$file) | grepl("^https?://", meta_video$file, ignore.case = TRUE)
vf <- NULL
if (any(!is_url)) vf <- tryCatch(fs::path_real(meta_video$file[!is_url]), error = function(e) NULL)
if (length(vf) < 1 && !any(is_url)) return(NULL)
## may have multiple video files at this point
for (thisf in unique(vf)) {
if (fs::file_exists(as.character(thisf))) {
## link_create doesn't allow files to be symlinked on windows, see https://github.com/r-lib/fs/issues/79
## we can only symlink directories
## so the symlink created in the servr root is a link to the directory containing the video file
symlink_abspath <- fs::path_abs(file.path(app_data$video_server_dir, digest::digest(fs::path_dir(thisf), algo = "sha1")))
do_create_this_symlink <- !(fs::link_exists(symlink_abspath) && fs::link_path(symlink_abspath) == fs::path_dir(thisf))
if (isTRUE(do_create_this_symlink)) {
suppressWarnings(try(unlink(symlink_abspath), silent = TRUE))
fs::link_create(fs::path_dir(thisf), symlink_abspath)
onStop(function() try({ unlink(symlink_abspath) }, silent = TRUE))
onSessionEnded(function() try({ unlink(symlink_abspath) }, silent = TRUE))
}
} else if (is_youtube_id(thisf) || grepl("https?://", thisf, ignore.case = TRUE)) {
## youtube ID or link to video, don't do anything
## should never get here, but just in case
} else {
## video file does not exist!
stop("video file ", thisf, " does not exist, not handled yet")
}
}
## windows causes us headaches here, because we can only symlink directories (not files), see above
## so we have to symlink the directory containing each video file
## that symlink will be given the hashed name of the video directory
path_hashes <- rep("", length(meta_video$file))
path_hashes[!is_url] <- vapply(fs::path_dir(fs::path_real(meta_video$file[!is_url])), digest::digest, algo = "sha1", FUN.VALUE = "")
## so the video_src is the symlink (i.e. hashed name, the directory) then the file name itself
meta_video$video_src <- paste_url(app_data$video_server_url, path_hashes, basename(meta_video$file))
## replace URLs with verbatim copy of original info
meta_video$video_src[is_url] <- meta_video$file[is_url]
} else if (is.function(app_data$video_serve_method)) {
if (nrow(meta_video) > 0) {
## block user interaction while this happens
showModal(modalDialog(title = "Please wait", size = "l", footer = NULL))
shiny::withProgress(message = "Processing video files", {
meta_video$video_src <- vapply(seq_nrows(meta_video), function(z) {
shiny::setProgress(value = z/nrow(meta_video))
tryCatch(app_data$video_serve_method(video_filename = meta_video$file[z], dvw_filename = meta_video$dvw_filename[z]), error = function(e) NA_character_)
}, FUN.VALUE = "", USE.NAMES = FALSE)
})
removeModal()
} else {
meta_video$video_src <- character()
}
} else if (is.string(app_data$video_serve_method) && app_data$video_serve_method %in% c("none")) {
## do nothing except pass the video file info into video_src
meta_video$video_src <- meta_video$file
} else {
stop("unrecognized video_serve_method: ", app_data$video_serve_method)
}
output$video_dialog <- renderUI({
if (any(is.na(meta_video$video_src) | !nzchar(meta_video$video_src))) {
tags$div(class = "alert alert-danger", "No video files for these match(es) could be found.")
} else if (any(is.na(meta_video$video_src) | !nzchar(meta_video$video_src))) {
tags$div(class = "alert alert-danger", "At least one video could not be found.")
} else {
NULL
}
})
meta_video[which(!is.na(meta_video$video_src) & nzchar(meta_video$video_src)), ]
}
})
build_playlist <- function(dat, meta_video) {
if ("home_score_start_of_point" %in% names(dat)) {
hsc <- "home_score_start_of_point"
vsc <- "visiting_score_start_of_point"
} else {
hsc <- "home_team_score"
vsc <- "visiting_team_score"
}
event_list <- mutate(dat, skill = case_when(.data$skill %in% c("Freeball dig", "Freeball over") ~ "Freeball", TRUE ~ .data$skill), ## ov_video needs just "Freeball"
skilltype = case_when(.data$skill %in% c("Serve", "Reception", "Dig", "Freeball", "Block", "Set") ~ .data$skill_type,
.data$skill == "Attack" ~ ifelse(is.na(.data$attack_description), .data$skill_type, .data$attack_description)),
subtitle = js_str_nospecials(paste("Set", .data$set_number, "-", .data$home_team, .data[[hsc]], "-", .data[[vsc]], .data$visiting_team)),
subtitleskill = js_str_nospecials(paste(.data$player_name, "-", .data$skilltype, ":", .data$evaluation_code)))
event_list <- dplyr::filter(event_list, !is.na(.data$video_time)) ## can't have missing video time entries
## note that the event_list can contain match_ids that do not appear in meta_video, if meta gets updated and the corresponding pbp_augment update is pending
if (!all(na.omit(event_list$match_id) %in% meta_video$match_id)) return(NULL) ## return NULL and await retrigger
## TODO: if we filter items out here because of missing video times (but not filter from the playstable), doesn't the playstable selected row get out of whack with the actual item being played?
vpt <- if (all(is_youtube_id(meta_video$video_src) | grepl("https?://.*youtube", meta_video$video_src, ignore.case = TRUE) | grepl("https?://youtu\\.be", meta_video$video_src, ignore.case = TRUE))) {
"youtube"
} else if (all(is_twitch_video(meta_video$video_src))) {
"twitch"
} else {
"local"
}
## TODO also check for mixed sources, which we can't handle yet
video_player_type(vpt)
if (!is.null(input$highlight_list)) {
pl <- ovideo::ov_video_playlist(x = event_list, meta = meta_video, type = vpt, timing = clip_timing(), extra_cols = c("match_id", "subtitle", "subtitleskill", plays_cols_to_show))
} else {
pl <- ovideo::ov_video_playlist(x = event_list, meta = meta_video, type = vpt, timing = clip_timing(), extra_cols = c("match_id", "subtitle", "subtitleskill", plays_cols_to_show))
}
pl <- pl[!is.na(pl$start_time) & !is.na(pl$duration), ]
## also keep track of actual file paths
left_join(pl, meta_video[, c("file", "video_src")], by = "video_src")
}
playlist <- reactiveVal(NULL)
last_playlist_hash <- ""
observe({
if (trace_execution) cat("recalculating playlist\n")
## Customize pbp
meta_video <- video_meta()
if (is.null(pbp_augment()) || nrow(pbp_augment()) < 1 || is.null(meta()) || is.null(selected_match_id()) || is.null(meta_video) || nrow(meta_video) < 1 || is.null(playstable_data()) || nrow(playstable_data()) < 1) {
## TODO does that need to be reactive to all of those things? Or just playstable_data() and video_meta() ?
playlist(NULL)
} else {
pl <- build_playlist(playstable_data(), meta_video = meta_video)
pl_hash <- digest::digest(pl)
if (last_playlist_hash != pl_hash) {
if (trace_execution) cat(" updating playlist\n")
last_playlist_hash <<- pl_hash
playlist(pl)
} else {
if (trace_execution) cat(" playlist has not changed\n")
}
}
})
clip_timing <- reactive({
## parse timing from inputs, with fallback to defaults if it fails
def0 <- def <- if (check_timing_df(app_data$video_timing_df)) app_data$video_timing_df else ovideo::ov_video_timing_df()
tryCatch({
## defaults
## need to explicitly list all the inputs to get reactivity, ergh
blah <- list(input$timing_serve_serve_start_offset, input$timing_serve_serve_duration,
input$timing_reception_reception_start_offset, input$timing_reception_reception_duration,
input$timing_set_reception_start_offset, input$timing_set_reception_duration,
input$timing_set_transition_start_offset, input$timing_set_transition_duration,
input$timing_attack_reception_start_offset, input$timing_attack_reception_duration,
input$timing_attack_transition_start_offset, input$timing_attack_transition_duration,
input$timing_block_reception_start_offset, input$timing_block_reception_duration,
input$timing_block_transition_start_offset, input$timing_block_transition_duration,
input$timing_dig_transition_start_offset, input$timing_dig_transition_duration,
input$timing_freeball_reception_start_offset, input$timing_freeball_reception_duration,
input$timing_freeball_transition_start_offset, input$timing_freeball_transition_duration)
for (ri in seq_nrows(def)) {
skill <- def$skill[ri]
phase <- def$phase[ri]
def$start_offset[ri] <- input[[paste0("timing_", tolower(skill), "_", tolower(phase), "_start_offset")]]
def$duration[ri] <- input[[paste0("timing_", tolower(skill), "_", tolower(phase), "_duration")]]
}
def
}, error = function(e) def0)
})
tweak_all_timings <- function(whch, by) {
if (whch %in% c("start_offset", "duration") && is.numeric(by)) {
def <- data.frame(skill = c("serve", "reception", "set", "set", "attack", "attack", "block", "block", "dig", "freeball", "freeball"),
phase = c("serve", "reception", "reception", "transition", "reception", "transition", "reception", "transition", "transition", "reception", "transition"),
stringsAsFactors = FALSE)
for (ri in seq_nrows(def)) {
thisid <- paste0("timing_", def$skill[ri], "_", def$phase[ri], "_", whch)
updateNumericInput(session, inputId = thisid, value = input[[thisid]] + by)
}
}
}
observeEvent(input$timing_all_start_minus, tweak_all_timings("start_offset", -1))
observeEvent(input$timing_all_start_plus, tweak_all_timings("start_offset", 1))
observeEvent(input$timing_all_duration_minus, tweak_all_timings("duration", -1))
observeEvent(input$timing_all_duration_plus, tweak_all_timings("duration", 1))
## video stuff
video_player_type <- reactiveVal("local") ## the current player type, either "local" or "youtube" or "twitch"
observe({
if (!is.null(playlist()) && nrow(playlist()) > 0) {
js_show("playstable");
if (trace_execution) cat("reinitializing video player\n")
## when playlist() changes, push it through to the javascript playlist
isolate({
was_paused <- isTRUE(input$player_pause_state)
suspended_state <- input$player_suspend_state
})
if (video_player_type() == "local") {
js_hide("dvyt_player")
js_show("dv_player")
} else {
js_hide("dv_player")
js_show("dvyt_player")
}
ov_video_control("stop", controller_var = "dvpl")
if (is_fresh_playlist) {
evaljs(ovideo::ov_playlist_as_onclick(playlist(), video_id = if (video_player_type() == "local") "dv_player" else "dvyt_player", dvjs_fun = "dvjs_set_playlist_and_play", seamless = TRUE, controller_var = "dvpl"))
} else {
## should only be here if the playlist was modified but playstable was NOT (i.e. we deleted an item from the playlist)
## so if we are mid-playlist already, do some other shenanigans so as not to restart from the first playlist item
evaljs(ovideo::ov_playlist_as_onclick(playlist(), video_id = if (video_player_type() == "local") "dv_player" else "dvyt_player", dvjs_fun = "dvjs_set_playlist", seamless = TRUE, controller_var = "dvpl")) ## set the playlist but don't auto-start playing (which would start from item 1)
evaljs(paste0("dvpl.video_controller.current=", master_playstable_selected_row - 1, ";")) ## set the current item
if (!isTRUE(input$player_suspend_state > 0)) { ##suspended_state < 1) {
## not suspended
## if we were paused, don't restart but set the player state to paused since it got reset when the new playlist was loaded
if (!was_paused) evaljs("dvpl.video_play();") else evaljs("dvpl.video_controller.paused=true;")
} else {
## the player was suspended, so set this one to the same state but otherwise do nothing, the unsuspend handler will take care of it
evaljs(paste0("dvpl.video_controller.suspended=", suspended_state, ";"))
}
}
} else {
js_hide("playstable");
## empty playlist, so stop the video, and clean things up
evaljs("dvpl.clear_playlist();")
## evaljs("remove_vspinner();") ## doesn't have an effect?
evaljs("if (document.getElementById(\"subtitle\")) { document.getElementById(\"subtitle\").textContent=\"Score\"; }; if (document.getElementById(\"subtitleskill\")) { document.getElementById(\"subtitleskill\").textContent=\"Skill\"; }")
}
})
output$player_controls_ui <- renderUI({
tags$div(tags$div(class = "player_controls", tags$button(tags$span(icon("play-circle", style = "vertical-align:middle;")), onclick = "dvpl.video_play();", title = "Play"),
tags$button(tags$span(icon("step-backward", style = "vertical-align:middle;")), onclick = "dvpl.video_prev();", title = "Previous"),
tags$button(tags$span(icon("step-forward", style = "vertical-align:middle;")), onclick = "dvpl.video_next(false);", title = "Next"),
tags$button(tags$span(icon("pause-circle", style = "vertical-align:middle;")), onclick = "dvpl.video_pause();", title = "Pause"),
tags$button(tags$span(icon("backward", style = "vertical-align:middle;"), " 1s"), onclick = "dvpl.jog(-1);", title = "Back 1 second"),
tags$button(tags$span(icon("expand", style = "vertical-align:middle;")), onclick = "dvpl.fullscreen();", title = "Full screen"),
tags$button(tags$span(icon("volume-mute", style = "vertical-align:middle;")), onclick = "dvpl.toggle_mute()", title = "Toggle mute")
),
tags$div(style="margin-top:10px;", tags$span(id = "subtitle", "Score"), tags$span(id = "subtitleskill", "Skill")))
})
clip_filename <- reactiveVal("")
clip_status <- reactiveVal(NULL)
output$create_clip_button_ui <- renderUI({
ok <- !is.null(playlist()) && nrow(playlist()) > 0 && !video_player_type() %in% c("youtube", "twitch")
## also check that videos are not remote - exclude videos served by http[s], but not if they are being served by the local (ovva-initiated) server
temp_src <- playlist()$video_src
if (is.string(app_data$video_serve_method) && app_data$video_serve_method %in% c("lighttpd", "servr")) {
is_local_url <- substr(temp_src, 1, nchar(app_data$video_server_url)) == app_data$video_server_url
temp_src[which(is_local_url)] <- "" ## these are ok
}
ok <- ok && !any(grepl("^https?://", temp_src, ignore.case = TRUE))
if (ok) {
actionButton("create_clip_button", "Download clip")
} else {
NULL
}
})
observeEvent(input$create_clip_button, {
ov_video_control("stop", controller_var = "dvpl")
showModal(modalDialog(title = "Create and download video clip", size = "l", "Please wait, creating clip. This could take some time.", uiOutput("clip_status_ui")))
## TODO - add progress indicator to that somehow? may not be possible with parallel
## do the video crunching
clip_status(NULL)
filename <- tempfile(fileext = ".mp4")
tryCatch({
ovideo::ov_ffmpeg_ok(do_error = TRUE)
future::plan("multisession")
pll <- lapply(seq_nrows(playlist()), function(z) as.list(playlist()[z, ])) ## need a non-reactive list-formatted copy of this to use with future_lapply
tempfiles <- future.apply::future_lapply(pll, function(plitem) {
##tempfiles <- lapply(pll), function(plitem) { ## for testing, no parallel
outfile <- tempfile(fileext = paste0(".", fs::path_ext(plitem$file)))
if (file.exists(outfile)) unlink(outfile)
infile <- tryCatch(fs::path_real(plitem$file), error = function(e) {
stop("file '", plitem$file, "' could not be resolved to a real file")
})
res <- sys::exec_internal(unname(ovideo::ov_ffmpeg_exe()), c("-ss", plitem$start_time, "-i", infile, "-strict", "-2", "-t", plitem$duration, outfile))
if (res$status != 0) stop("failed to get video clip, ", rawToChar(res$stderr))
outfile
})
tempfiles <- unlist(tempfiles)
cfile <- tempfile(fileext = ".txt")
on.exit(unlink(c(cfile, tempfiles)))
cat(paste0("file ", tempfiles), file = cfile, sep = "\n")
if (file.exists(filename)) unlink(filename)
res <- sys::exec_internal(unname(ovideo::ov_ffmpeg_exe()), c("-safe", 0, "-f", "concat", "-i", cfile, "-c", "copy", filename))
if (res$status != 0) stop("failed to combine clips, ", rawToChar(res$stderr))
clip_filename(filename)
removeModal()
showModal(modalDialog(title = "Create and download video clip", size = "l", shiny::downloadButton("download_clip")))
}, error = function(e) {
clip_status(conditionMessage(e))
})
})
output$clip_status_ui <- renderUI({
if (is.null(clip_status())) {
NULL
} else {
tags$div(class = "alert alert-danger", "Sorry, something went wrong. The error message was: ", clip_status())
}
})
output$download_clip <- shiny::downloadHandler(
filename = function() {
filename <- paste0("ovva_highlights.mp4")
},
content = function(file) {
removeModal()
file.copy(clip_filename(), file)
},
contentType = "video/mp4"
)
observeEvent(input$playback_rate, {
if (!is.null(input$playback_rate)) ov_video_control("set_playback_rate", input$playback_rate, controller_var = "dvpl")
})
pl_can_be_saved <- reactive({
!is.null(playlist()) && (nrow(playlist()) > 0)
})
output$chart_ui <- renderUI({
out <- list(tags$div(style = "height:24px;"),
if (pl_can_be_saved()) downloadButton("download_playlist", label = "Download playlist CSV"),
uiOutput("create_clip_button_ui", inline = TRUE),
uiOutput("chart2_ui"))
do.call(tagList, Filter(Negate(is.null), out))
})
output$chart2_ui <- renderUI(app_data$chart_renderer)
## download playlist
output$download_playlist <- downloadHandler(filename = "playlist.csv",
content = function(file) write.csv(playlist(), file, row.names = FALSE, fileEncoding = "UTF-8"))
## height of the video player element
vo_height <- reactiveVal("auto")
observe({
my_height <- if (video_player_type() %in% c("youtube", "twitch")) input$dvyt_height else input$dv_height
if (!is.null(my_height) && as.numeric(my_height) > 0) {
vo_height(as.numeric(my_height))
## +1 because of 1px border on video element
evaljs(paste0("document.getElementById('video_overlay').style.height = '", vo_height()+1, "px';"))
evaljs(paste0("document.getElementById('video_overlay_img').style.height = '", vo_height()+1, "px';"))
evaljs(paste0("document.getElementById('dv_h_overlay').style.height = '", vo_height()+1, "px';"))
} else {
vo_height("auto")
evaljs(paste0("document.getElementById('video_overlay').style.height = '400px';"))
evaljs(paste0("document.getElementById('video_overlay_img').style.height = '400px';"))
evaljs(paste0("document.getElementById('dv_h_overlay').style.height = '400px';"))
}
})
## width of the video player element
vo_width <- reactiveVal("auto")
observe({
my_width <- if (video_player_type() %in% c("youtube", "twitch")) input$dvyt_width else input$dv_width
if (!is.null(my_width) && as.numeric(my_width) > 0) {
vo_width(as.numeric(my_width))
evaljs(paste0("document.getElementById('video_overlay_img').style.width = '", vo_width()+1, "px';"))
evaljs(paste0("document.getElementById('dv_h_overlay').style.width = '", vo_width()+1, "px';"))
} else {
vo_width("auto")
evaljs(paste0("document.getElementById('video_overlay_img').style.width = '600px';"))
evaljs(paste0("document.getElementById('dv_h_overlay').style.width = '600px';"))
}
})
## height of the video player container, use as negative vertical offset on the overlay element
observe({
if (!is.null(input$vo_voffset) && as.numeric(input$vo_voffset) > 0) {
evaljs(paste0("document.getElementById('video_overlay').style.marginTop = '-", input$vo_voffset, "px';"))
evaljs(paste0("document.getElementById('video_overlay_img').style.marginTop = '-", input$vo_voffset, "px';"))
evaljs(paste0("document.getElementById('dv_h_overlay').style.marginTop = '-", input$vo_voffset, "px';"))
} else {
evaljs("document.getElementById('video_overlay').style.marginTop = '0px';")
evaljs("document.getElementById('video_overlay_img').style.marginTop = '0px';")
evaljs("document.getElementById('dv_h_overlay').style.marginTop = '0px';")
}
})
## panel show/hide
panel_visible <- reactiveValues(filter2 = FALSE)
observeEvent(input$collapse_filter2, {
if (panel_visible$filter2) js_hide("filter2_panel") else js_show("filter2_panel")
panel_visible$filter2 <- !panel_visible$filter2
})
observe({
if (panel_visible$filter2) updateActionButton(session, "collapse_filter2", label = "Hide") else updateActionButton(session, "collapse_filter2", label = "Show")
})
observeEvent(input$video_error, {
vid_err_msgs <- c("video playback was aborted", "a network error caused the video download to fail", "an error occurred while trying to decode the video", "the video could not be loaded or the format is not supported")
temp <- if (!is.null(input$video_error) && nzchar(input$video_error) && grepl("@", input$video_error)) {
tryCatch(strsplit(input$video_error, "@")[[1]], error = function(e) NULL)
} else {
NULL
}
if (length(temp) > 2) {
errmsg <- if (as.numeric(temp[3]) %in% 1:4) vid_err_msgs[as.numeric(temp[3])] else "unknown error"
this_src <- tryCatch(rawToChar(base64enc::base64decode(temp[2])), error = function(e) "unknown")
if (length(this_src) < 1 || !is.character(this_src) || !nzchar(this_src)) this_src <- "unknown"
} else {
errmsg <- "unknown error"
this_src <- "unknown"
}
##status_msg <- NULL
if (length(this_src == 1) && is.character(this_src) && grepl("^https?://", this_src, ignore.case = TRUE)) {
## ## can we get the http status?
## status_msg <- tryCatch(httr::http_status(httr::HEAD(this_src))$message, error = function(e) if (grepl("Connection refused", conditionMessage(e), ignore.case = TRUE)) "Connection refused" else NULL)
this_src <- tags$a(href = this_src, this_src, target = "_blank")
}
output$video_dialog <- renderUI(tags$div(class = "alert alert-danger", tags$div("Video error ", paste0("(", errmsg, "). Is the video URL correct?"), tags$br(), "Video source: ", this_src)))
})
}
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.