diff --git a/app/assets/javascripts/media_player_wrapper/avalon_player_new.es6 b/app/assets/javascripts/media_player_wrapper/avalon_player_new.es6 index 9163d0323c..3d310ee7bb 100644 --- a/app/assets/javascripts/media_player_wrapper/avalon_player_new.es6 +++ b/app/assets/javascripts/media_player_wrapper/avalon_player_new.es6 @@ -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; @@ -157,7 +156,6 @@ class MEJSPlayer { this.setContextVars(response, playlistItemsT); this.createNewPlayer(); } - this.updateShareLinks(); }) .fail(error => { console.log('error', error); @@ -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); - } } diff --git a/app/assets/javascripts/media_player_wrapper/mejs4_helper_quality.es6 b/app/assets/javascripts/media_player_wrapper/mejs4_helper_quality.es6 deleted file mode 100644 index 655eabf5df..0000000000 --- a/app/assets/javascripts/media_player_wrapper/mejs4_helper_quality.es6 +++ /dev/null @@ -1,58 +0,0 @@ -// 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. - -/** - * Quality helper class to add functionality to stock MEJS quality plugin - * @class MEJSQualityHelper - */ -class MEJSQualityHelper { - constructor() { - $(document).on( - 'mejs4handleSuccess', - this.addQualitySelectorListeners.bind(this) - ); - } - - /** - * Add event listeners to send quality selections to server - * @function addQualitySelectorListeners - * @return {void} - */ - addQualitySelectorListeners() { - const player = mejs4AvalonPlayer.player; - if (!player.qualitiesButton) { return } - const radios = player.qualitiesButton.querySelectorAll( - 'input[type="radio"]' - ); - - for (let i = 0, total = radios.length; i < total; i++) { - const radio = radios[i]; - radio.addEventListener('change', this.updateQualitySelection.bind(this)); - } - } - - updateQualitySelection(e) { - const quality = e.target.value; - mejs4AvalonPlayer.defaultQuality = quality; - this.sendQualitySelection(quality); - } - - sendQualitySelection(quality) { - $.ajax({ - type: 'POST', - url: '/media_objects/set_session_quality', - data: { quality: quality }, - dataType: 'json' - }); - } -} diff --git a/app/assets/javascripts/media_player_wrapper/mejs4_helper_utility.es6 b/app/assets/javascripts/media_player_wrapper/mejs4_helper_utility.es6 index 7a0de9727e..635bd9f5f6 100644 --- a/app/assets/javascripts/media_player_wrapper/mejs4_helper_utility.es6 +++ b/app/assets/javascripts/media_player_wrapper/mejs4_helper_utility.es6 @@ -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(); - } } diff --git a/app/assets/javascripts/ramp_utils.js b/app/assets/javascripts/ramp_utils.js new file mode 100644 index 0000000000..570ce51821 --- /dev/null +++ b/app/assets/javascripts/ramp_utils.js @@ -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); +} diff --git a/app/assets/stylesheets/avalon.scss b/app/assets/stylesheets/avalon.scss index 2e34a79336..5f17e0faf2 100644 --- a/app/assets/stylesheets/avalon.scss +++ b/app/assets/stylesheets/avalon.scss @@ -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; } diff --git a/app/helpers/media_objects_helper.rb b/app/helpers/media_objects_helper.rb index d9c1db206d..9d67c7f9be 100644 --- a/app/helpers/media_objects_helper.rb +++ b/app/helpers/media_objects_helper.rb @@ -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 ? '' : '' - playlist_btn = current_ability.can?(:create, Playlist) ? "" : '' - - headeropen = < -
- #{playlist_btn} -EOF - headerclose = < - -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}
  • #{link}
#{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 = < - -
  • #{link}
- #{headerclose} - -
-
-
    -EOF - wrapperclose = < -
-
-EOF - # If there are no subsections within the structure, return just the header with the single section - else - tracknumber = index - wrapperopen = "#{headeropen}
    " - wrapperclose = "
#{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? diff --git a/app/javascript/components/Ramp.jsx b/app/javascript/components/Ramp.jsx new file mode 100644 index 0000000000..94a24686dd --- /dev/null +++ b/app/javascript/components/Ramp.jsx @@ -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 ( + + +
+ { timeline.canCreate &&
} + { share.canShare &&
} +
+ + + + + + + + + + ); +}; + +export default Ramp; diff --git a/app/javascript/components/Ramp.scss b/app/javascript/components/Ramp.scss new file mode 100644 index 0000000000..38eacf334d --- /dev/null +++ b/app/javascript/components/Ramp.scss @@ -0,0 +1,26 @@ +.iiif-player { + .ramp--structured-nav { + float: left; + width: 40%; + margin-top: 20px; + } + .ramp--transcript_nav { + float: right; + width: 60%; + margin-left: -20px; + margin-top: 20px; + } +} + +.ramp--rails-content { + display: flex; + + .share-tabs { + flex-grow: 2; + } + + #share-list { + margin-left: -9.25rem; + } + +} diff --git a/app/javascript/components/ReactIIIFTranscript.jsx b/app/javascript/components/ReactIIIFTranscript.jsx deleted file mode 100644 index 14d3663ae5..0000000000 --- a/app/javascript/components/ReactIIIFTranscript.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import { Transcript } from "@samvera/iiif-react-media-player"; -import 'video.js/dist/video-js.css'; -import "@samvera/iiif-react-media-player/dist/iiif-react-media-player.css"; - -const ReactIIIFTranscript = ({ base_url, transcripts }) => { - const [transcriptsProp, setTrancsriptProp] = React.useState([]); - // Check for at least one masterfile in the mediaobject has a transcript file - const [hasTranscript, setHasTranscript] = React.useState(false); - - React.useEffect(() => { - buildTranscriptUrls(); - }, []) - - const buildTranscriptUrls = () => { - let trProps = []; - transcripts.forEach((tr, i) => { - let transcriptItems = tr.transcripts; - let canvasTrs = { canvasId: i, items: [] } - - // construct URLs as expected within the transcript component - if(transcriptItems.length > 0) { - setHasTranscript(true) - canvasTrs.items = transcriptItems.map( - t => ( - { title: t.label, - url: `${base_url}/master_files/${tr.id}/transcript/${t.id}` - } - ) - ) - } - trProps.push(canvasTrs) - }); - setTrancsriptProp(trProps) - } - - // Render the transcript component if at least one masterfile (canvas in manifest) - // has a transcript file - if(hasTranscript) { - return ( -
- -
- ); - } else { - return null; - } -} - -export default ReactIIIFTranscript; diff --git a/app/views/media_objects/_item_view.html.erb b/app/views/media_objects/_item_view.html.erb index 4345c0136a..ab7fea52f3 100644 --- a/app/views/media_objects/_item_view.html.erb +++ b/app/views/media_objects/_item_view.html.erb @@ -29,20 +29,16 @@ Unless required by applicable law or agreed to in writing, software distributed <% if lending_enabled?(@media_object) && !can_stream %> <%= render 'embed_checkout' %> <% elsif @currentStream %> - <%= render partial: "modules/player/section", locals: {section: @currentStream, section_info: @currentStreamInfo, f_start: @f_start, f_end: @f_end} %> - <%= render file: '_track_scrubber.html.erb' if is_mejs_2? %> - <%= render file: '_add_to_playlist.html.erb' if current_user.present? && is_mejs_2? %> - <%# Partial view for MEJS4 Add To Playlist plugin %> - <%= render partial: 'mejs4_add_to_playlist' if current_user.present? && is_mejs_4? %> <%= render 'workflow_progress' %> - <%= render partial: 'timeline' if current_ability.can? :create, Timeline %> - <%= render 'share' if will_partial_list_render? :share %> - - - <%= render partial: 'sections', - locals: { mediaobject: @media_object, - sections: @masterFiles, - activeStream: @currentStream } %> + <%= react_component("Ramp", + { + base_url: request.protocol+request.host_with_port, + mo_id: @media_object.id, + canvas_count: @media_object.master_files.size, + share: { canShare: (will_partial_list_render? :share), content: render('share') }, + timeline: { canCreate: (current_ability.can? :create, Timeline), content: render('timeline') } + } + ) %> <% end %>
@@ -52,32 +48,29 @@ Unless required by applicable law or agreed to in writing, software distributed
<%= render "metadata_display" %>
- - <% if can_stream && @currentStream %> - <% supplemental_files = @masterFiles.map { |mf| {"id" => mf.id, "transcripts" => mf.supplemental_files(tag: 'transcript') } }.flatten(1) %> - <%= react_component("ReactIIIFTranscript", {base_url: request.protocol+request.host_with_port, transcripts: supplemental_files }) %> - <% end %>
<% content_for :page_scripts do %> - <% if @currentStream.present? and @currentStream.derivatives.present? %> - <%= render partial: "mejs4_player_js", locals: {section: @currentStream, section_info: @currentStreamInfo} %> - <% end %> - diff --git a/app/views/media_objects/_sections.html.erb b/app/views/media_objects/_sections.html.erb deleted file mode 100644 index 4dfff4d855..0000000000 --- a/app/views/media_objects/_sections.html.erb +++ /dev/null @@ -1,129 +0,0 @@ -<%# -Copyright 2011-2023, 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. ---- END LICENSE_HEADER BLOCK --- -%> -<% show_progress = show_progress?(sections) %> -<% unless hide_sections?(sections) and not show_progress %> - -
-
- - -<% show_all_sections = can? :edit, @media_object %> -<% sections.each_with_index do |section,i| %> -<% unless show_all_sections %> -<% next if section.derivatives.empty? %> -<% end %> -<%= structure_html(section,i,show_progress).html_safe %> -<% end %> - -
-
- - - - -<% content_for :page_scripts do %> - -<% end %> -<% end %> diff --git a/app/views/media_objects/_share.html.erb b/app/views/media_objects/_share.html.erb index 74c6a89f66..340d318e88 100644 --- a/app/views/media_objects/_share.html.erb +++ b/app/views/media_objects/_share.html.erb @@ -32,21 +32,44 @@ Unless required by applicable law or agreed to in writing, software distributed <% content_for :page_scripts do %> <% end %> diff --git a/app/views/media_objects/_timeline.html.erb b/app/views/media_objects/_timeline.html.erb index e25e125a1e..d7801b3c25 100644 --- a/app/views/media_objects/_timeline.html.erb +++ b/app/views/media_objects/_timeline.html.erb @@ -20,7 +20,7 @@ Unless required by applicable law or agreed to in writing, software distributed -