Skip to content

Commit

Permalink
Ramp poc (#5277)
Browse files Browse the repository at this point in the history
* PoC for Ramp integration

* Fix webpack error with new Ramp import

* Remove MEJS references as much as possible without breaking playlists

* Downgrade react to 17, render rails templates inside React component

* Fix create timeline modal with Ramp integration

* Set share panel URLs when canvas changes

* Cleanup in some JS, HTML, and helper code and tar file for compiled ramp

* Comment tests

* Fix tests
Co-author: cjcolvar@iu.edu
  • Loading branch information
Dananji authored Aug 15, 2023
1 parent d8906a9 commit 6364985
Show file tree
Hide file tree
Showing 17 changed files with 467 additions and 639 deletions.
20 changes: 0 additions & 20 deletions app/assets/javascripts/media_player_wrapper/avalon_player_new.es6
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ class MEJSPlayer {
this.mejsUtility = new MEJSUtility();
this.mejsTimeRailHelper = new MEJSTimeRailHelper();
this.mejsMarkersHelper = new MEJSMarkersHelper();
this.mejsQualityHelper = new MEJSQualityHelper();
this.localStorage = window.localStorage;
this.canvasIndex = 0;

Expand Down Expand Up @@ -157,7 +156,6 @@ class MEJSPlayer {
this.setContextVars(response, playlistItemsT);
this.createNewPlayer();
}
this.updateShareLinks();
})
.fail(error => {
console.log('error', error);
Expand Down Expand Up @@ -785,22 +783,4 @@ class MEJSPlayer {
const currentIdIndex = [...sectionsIdArray].indexOf(currentStreamInfo.id);
this.canvasIndex = currentIdIndex;
}

/**
* Update section and lti section share links and embed code when switching sections
* @function updateShareLinks
* @return {void}
*/
updateShareLinks() {
const sectionShareLink = this.currentStreamInfo.link_back_url;
const ltiShareLink = this.currentStreamInfo.lti_share_link;
const embedCode = this.currentStreamInfo.embed_code;
$('#share-link-section')
.val(sectionShareLink)
.attr('placeholder', sectionShareLink);
$('#ltilink-section')
.val(ltiShareLink)
.attr('placeholder', ltiShareLink);
$('#embed-part').val(embedCode);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -184,63 +184,4 @@ class MEJSUtility {
player.startControlsTimer();
}
}

/**
* Get new timeline scopes for active section playing
* @function timelineScopes
* @param {player} player - reference to currentPlayer
* @return {[{string, int, string}]} [{label, tracks, t}] = scope label, number of tracks, mediafragment
*/
timelineScopes(player) {
let duration = player.duration;
let scopes = new Array();
let trackCount = 1;
const currentStream = $('#accordion li a.current-stream');

if (
currentStream.length > 0 &&
!currentStream.closest('div').hasClass('card-header')
) {
let $firstCurrentStream = $(currentStream[0]);
let re1 = /^\s*\d\.\s*/; // index number in front of section title '1. '
let re2 = /\s*\(.*\)$/; // duration notation at end of section title ' (2:00)'
let label = $firstCurrentStream
.text()
.replace(re1, '')
.replace(re2, '')
.trim();
let begin =
parseFloat($firstCurrentStream[0].dataset['fragmentbegin']) || 0;
let end =
parseFloat($firstCurrentStream[0].dataset['fragmentend']) || duration;

scopes.push({
label: label,
tracks: trackCount,
t: 't=' + begin + ',' + end
});

let parent = $firstCurrentStream.closest('ul').closest('li');

while (parent.length > 0) {
let tracks = parent.find('li a');
trackCount = tracks.length;
begin = parseFloat(tracks[0].dataset['fragmentbegin']) || 0;
end = parseFloat(tracks[trackCount - 1].dataset['fragmentend']) || '';
scopes.push({
label: parent.prev().text().trim(),
tracks: trackCount,
t: 't=' + begin + ',' + end
});
parent = parent.closest('ul').closest('li');
}
}
trackCount = currentStream.closest('div').find('li a').length;
scopes.push({
label: player.avalonWrapper.currentStreamInfo.embed_title,
tracks: trackCount,
t: 't=0,'
});
return scopes.reverse();
}
}
96 changes: 96 additions & 0 deletions app/assets/javascripts/ramp_utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright 2011-2022, The Trustees of Indiana University and Northwestern
// University. Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
//
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.


/**
* Get new timeline scopes for active section playing
* @function getTimelineScopes
* @param title title of the mediaobject
* @return { [{string, int, string}], string } { [{label, tracks, t}], streamId } = [scope label, number of tracks, mediafragment], masterfile id
*/
function getTimelineScopes(title) {
let scopes = new Array();
let trackCount = 1;
let currentPlayer = document.getElementById('iiif-media-player');
let duration = currentPlayer.player.duration();
let currentStructureItem = $('li[class="ramp--structured-nav__list-item active"]');

let item = currentStructureItem[0].childNodes[1]
let label = item.text;
let times = item.hash.split('#t=').reverse()[0];
let begin = parseFloat(times.split(',')[0]) || 0;
let end = parseFloat(times.split(',')[1]) || duration;
let streamId = item.pathname.split('/').reverse()[0];
scopes.push({
label: label,
tracks: trackCount,
t: `t=${begin},${end}`,
});

let parent = currentStructureItem.closest('ul').closest('li');
while (parent.length > 0) {
let next = parent.closest('ul').closest('li');
let tracks = parent.find('li a');
trackCount = tracks.length;
begin = parseFloat(tracks[0].hash.split('#t=').reverse()[0].split(',')[0]) || 0;
end = parseFloat(tracks[trackCount - 1].hash.split('#t=').reverse()[0].split(',')[1]) || '';
streamId = tracks[0].pathname.split('/').reverse()[0];
label = parent[0].childNodes[0].textContent;
scopes.push({
label: next.length == 0 ? `${title} - ${label}` : label,
tracks: trackCount,
t: `t=${begin},${end}`,
});
parent = next;
}
return { scopes: scopes.reverse(), streamId };
}

function createTimestamp(secTime, showHrs) {
let hours = Math.floor(secTime / 3600);
let minutes = Math.floor((secTime % 3600) / 60);
let seconds = secTime - minutes * 60 - hours * 3600;
if (seconds > 59.9) {
minutes = minutes + 1;
seconds = 0;
}
seconds = parseInt(seconds);

let hourStr = hours < 10 ? `0${hours}` : `${hours}`;
let minStr = minutes < 10 ? `0${minutes}` : `${minutes}`;
let secStr = seconds < 10 ? `0${seconds}` : `${seconds}`;

let timeStr = `${minStr}:${secStr}`;
if (showHrs || hours > 0) {
timeStr = `${hourStr}:${timeStr}`;
}
return timeStr;
}

/**
* Update section and lti section share links and embed code when switching sections
* @function updateShareLinks
* @return {void}
*/
function updateShareLinks (e) {
const sectionShareLink = e.detail.link_back_url;
const ltiShareLink = e.detail.lti_share_link;
const embedCode = e.detail.embed_code;
$('#share-link-section')
.val(sectionShareLink)
.attr('placeholder', sectionShareLink);
$('#ltilink-section')
.val(ltiShareLink)
.attr('placeholder', ltiShareLink);
$('#embed-part').val(embedCode);
}
4 changes: 2 additions & 2 deletions app/assets/stylesheets/avalon.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1200,9 +1200,9 @@ td {

/*
Override CSS for transcript component imported
from @samvera/iiif-react-media-player
from @samvera/ramp
*/
.irmp--transcript_nav {
.ramp--transcript_nav {
padding: 10px 0 0 0;
}

Expand Down
71 changes: 0 additions & 71 deletions app/helpers/media_objects_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,77 +138,6 @@ def any_failed?(sections)
ActiveEncode::EncodeRecord.where(global_id: encode_gids).any? { |encode| encode.state.to_s.upcase == 'FAILED' }
end

def hide_sections? sections
sections.blank? or (sections.length == 1 and !sections.first.has_structuralMetadata?)
end

def structure_html section, index, show_progress
current = is_current_section? section
progress_div = show_progress ? '<div class="status-detail alert" style="display: none"></div>' : ''
playlist_btn = current_ability.can?(:create, Playlist) ? "<button type=\"button\" title=\"Add section to playlist\" aria-label=\"Add section to playlist\" class=\"structure_add_to_playlist btn btn-primary\" data-scope=\"master_file\" data-masterfile-id=\"#{section.id}\"></button>" : ''

headeropen = <<EOF
<div class="card-header" role="tab" id="heading#{index}" data-media-object-id="#{section.media_object_id}" data-section-id="#{section.id}">
<h5 class="card-title #{ 'progress-indented' if progress_div.present? }">
#{playlist_btn}
EOF
headerclose = <<EOF
#{progress_div}
</h5>
</div>
EOF

data = {
segment: section.id,
is_video: section.file_format != 'Sound',
share_link: share_link_for(section),
native_url: id_section_media_object_path(@media_object, section.id)
}
data[:lti_share_link] = user_omniauth_callback_lti_url(target_id: section) if Avalon::Authentication::Providers.any? {|p| p[:provider] == :lti }
duration = section.duration.blank? ? '' : " (#{milliseconds_to_formatted_time(section.duration.to_i, false)})"

# If there is no structural metadata associated with this master_file return the stream info
unless section.has_structuralMetadata?
label = "#{index+1}. #{stream_label_for(section)} #{duration}".html_safe
link = link_to label, share_link_for( section ), id: 'section-title-' + section.id, data: data, class: 'playable wrap' + (current ? ' current-stream current-section' : '')
return "#{headeropen}<ul><li class='stream-li'>#{link}</li></ul>#{headerclose}"
end

sectionnode = section.structuralMetadata.xpath('//Item')

# If there are subsections within structure, build a collapsible panel with the contents
if sectionnode.children.present?
tracknumber = 0
label = "#{index+1}. #{sectionnode.attribute('label').value} #{duration}".html_safe
link = link_to label, share_link_for(section, only_path: true),
id: 'section-title-' + section.id, data: data,
class: 'playable wrap' + (current ? ' current-stream current-section' : '')
wrapperopen = <<EOF
#{headeropen}
<button class="fa fa-minus-square #{current ? '' : 'no-show'}" data-toggle="collapse" data-target="#section#{index}" aria-expanded="#{current ? 'true' : 'false' }" aria-controls="collapse#{index}"></button>
<button class="fa fa-plus-square #{current ? 'no-show' : ''}" data-toggle="collapse" data-target="#section#{index}" aria-expanded="#{current ? 'true' : 'false' }" aria-controls="collapse#{index}"></button>
<ul><li>#{link}</li></ul>
#{headerclose}
<div id="section#{index}" class="panel-collapse collapse #{current ? 'show' : ''}" role="tabpanel" aria-labelledby="heading#{index}">
<div class="card-body">
<ul>
EOF
wrapperclose = <<EOF
</ul>
</div>
</div>
EOF
# If there are no subsections within the structure, return just the header with the single section
else
tracknumber = index
wrapperopen = "#{headeropen}<ul>"
wrapperclose = "</ul>#{headerclose}"
end
contents, tracknumber = parse_section section, sectionnode.first, tracknumber
"#{wrapperopen}#{contents}#{wrapperclose}"
end

def parse_section section, node, index
sectionnode = section.structuralMetadata.xpath('//Item')
if sectionnode.children.present?
Expand Down
50 changes: 50 additions & 0 deletions app/javascript/components/Ramp.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';
import { Transcript, IIIFPlayer, MediaPlayer, StructuredNavigation } from "@samvera/ramp";
import 'video.js/dist/video-js.css';
import "@samvera/ramp/dist/ramp.css";
import { Col, Row } from 'react-bootstrap';
import './Ramp.scss';

const Ramp = ({ base_url, mo_id, canvas_count, share, timeline }) => {
const [transcriptsProp, setTrancsriptProp] = React.useState([]);
const [manifestUrl, setManifestUrl] = React.useState('');

React.useEffect(() => {
let url = `${base_url}/media_objects/${mo_id}/manifest.json`;
setManifestUrl(url);
buildTranscripts(url);
}, []);

const buildTranscripts = (url) => {
let trProps = [];
for(let i = 0; i < canvas_count; i++) {
let canvasTrs = { canvasId: i, items: [] };
canvasTrs.items = [{ title: '', url }];
trProps.push(canvasTrs);
}
setTrancsriptProp(trProps);
};

return (
<IIIFPlayer manifestUrl={manifestUrl}>
<MediaPlayer enableFileDownload={false} />
<div className="ramp--rails-content">
{ timeline.canCreate && <div className="mr-1" dangerouslySetInnerHTML={{ __html: timeline.content }} /> }
{ share.canShare && <div className="share-tabs" dangerouslySetInnerHTML={{ __html: share.content }} /> }
</div>
<Row>
<Col>
<StructuredNavigation />
</Col>
<Col>
<Transcript
playerID="iiif-media-player"
transcripts={transcriptsProp}
/>
</Col>
</Row>
</IIIFPlayer>
);
};

export default Ramp;
Loading

0 comments on commit 6364985

Please sign in to comment.