Panopto Embed Captioning Problem Solver

You got a problem? I got a problem solver.1

The problem was that we had videos that had transcript text already written in the video. Then, for some reason, Panopto insisted on displaying the captions no matter what the iframe parameters said. That resulted in captions over captions in a way that made everything hard to read.

After a while of no progress, the faculty member and students started hand-deleting the captions in some laborious and painful way . . . I was also getting less than helpful suggestions via AI about non-functional/impossible CSS to fix the problem.2

I decided to see if the Panopto embed API would let me fix the problem. I’d messed around with it at various times of the last several years so I was semi-familiar. Turns out there’s a function called disableCaptions() that works.

I tested it in CodePen. The first video is a regular embed, the second is via the API with captions disabled.

Now that I knew it worked, I needed to transfer it into a WordPress plugin that would parse out the existing iframe embeds and redo them as API embeds. I could have asked them to handwrite short codes or something similar but technology really ought to be able to handle crap like this.

The following is a content filter that finds all the iframe embeds with panopto in the URL, then it rewrites the content as a div with a unique id and the video id as a data parameter. I love data parameters.

add_filter( 'the_content', 'panopto_cure_clean_iframe', 1 );

function panopto_cure_clean_iframe($content){
     $pattern = '/<iframe[^>]+src=["\']([^"\']*panopto[^"\']*)["\'][^>]*>.*?<\/iframe>/is';

    $i = 0;
    $cleaned_text = preg_replace_callback($pattern, function($matches) use (&$panopto_urls, &$i) {
        $url = $matches[1];
        $panopto_urls[] = $url;
        $id = panopto_cure_id($url);
        $shortcode = "<div class='player' id='player-{$i}' data-session-id='{$id}'></div>";
        $i++;
        return $shortcode;
    }, $content);

    return $cleaned_text;
}

function panopto_cure_id($url){
    $parsed_url = parse_url($url);

    if (!isset($parsed_url['query'])) {
        return null; // No query string
    }
    parse_str($parsed_url['query'], $query_params);

    return $query_params['id'] ?? null;
}


function extract_iframe_urls($text) {
    $iframe_urls = [];
    $pattern = '/<iframe[^>]+src=["\']([^"\']+)["\']/i';

    if (preg_match_all($pattern, $text, $matches)) {
        $iframe_urls = $matches[1]; // [1] contains the captured src values
    }

    return $iframe_urls;
}

The javascript has to become a bit smarter to handle multiple videos.

[javscript]
// Load the IFrame player API code asynchronously
const loadScript = () => {
const tag = document.createElement(‘script’);
tag.src = “https://developers.panopto.com/scripts/embedapi.min.js”;
const firstScriptTag = document.getElementsByTagName(‘script’)[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
};

// Store all player instances
const players = {};

// Function to initialize a player
const initializePlayer = (playerId, sessionId) => {
players[playerId] = new EmbedApi(playerId, {
width: “750”,
height: “422”,
serverName: “midd.hosted.panopto.com”,
sessionId: sessionId,
videoParams: {
“interactivity”: “all”,
“showtitle”: “false”,
},
events: {
“onIframeReady”: () => onPanoptoIframeReady(playerId),
“onReady”: () => onPanoptoVideoReady(playerId),
“onStateChange”: (state) => onPanoptoStateUpdate(playerId, state)
}
});
};

// The API will call this function when the iframe is ready
const onPanoptoIframeReady = (playerId) => {
// The iframe is ready and the video is not yet loaded
players[playerId].disableCaptions();
};

// The API will call this function when the video player is ready
const onPanoptoVideoReady = (playerId) => {
// The video has successfully been loaded
players[playerId].playVideo();
};

// The API calls this function when a player state change happens
const onPanoptoStateUpdate = (playerId, state) => {
if (state === PlayerState.Playing) {
players[playerId].setVolume(1);
players[playerId].setPlaybackRate(1);
}
};

// Initialize when API is ready
function onPanoptoEmbedApiReady() {
// Find all player divs with data-session-id attribute
const playerDivs = document.querySelectorAll(‘[data-session-id]’);

// Initialize each player
playerDivs.forEach(div => {
const playerId = div.id;
const sessionId = div.getAttribute(‘data-session-id’);

if (playerId && sessionId) {
initializePlayer(playerId, sessionId);
}
});
}

// Load the script
loadScript();
[/javascript]


1 Actually it’s just programming.

2 To be clear, I’m not mocking the person but the AI did very poorly here. That’s not even valid CSS. And it wouldn’t work if it was.