Skip to content

Commit aedc5de

Browse files
committed
Add caption search feature
1 parent b860a05 commit aedc5de

10 files changed

Lines changed: 485 additions & 5 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ The following attributes are supported on both the \<audio\> and \<video\> eleme
190190
download as much of the media as possible. If the media is not a
191191
central focus, downloading the entire media resource can consume
192192
valuable bandwidth, so preload=“metadata” would be a better option.
193-
193+
**data-search** - optional; search terms to search for within the caption tracks, separated by a space
194+
**data-search-div** - optional; id of external container in which to display search results
194195

195196
The following attributes are supported on the \<video\> element only:
196197

build/ableplayer.js

Lines changed: 184 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,14 @@
177177
this.metaDiv = $(media).data('meta-div');
178178
}
179179

180+
if ($(media).data('search') !== undefined && $(media).data('search') !== "") {
181+
// conducting a search currently requires an external div in which to write the results
182+
if ($(media).data('search-div') !== undefined && $(media).data('search-div') !== "") {
183+
this.searchString = $(media).data('search');
184+
this.searchDiv = $(media).data('search-div');
185+
}
186+
}
187+
180188
this.ableIndex = AblePlayer.nextIndex;
181189
AblePlayer.nextIndex += 1;
182190

@@ -624,6 +632,7 @@
624632
thisObj.updateDescription();
625633
thisObj.updateCaption();
626634
thisObj.updateTranscript();
635+
thisObj.showSearchResults();
627636
thisObj.initializing = false;
628637
thisObj.refreshControls();
629638

@@ -5405,16 +5414,189 @@
54055414
return main;
54065415
};
54075416
})();
5417+
(function () {
5418+
AblePlayer.prototype.showSearchResults = function() {
5419+
5420+
// search VTT file for all instances of searchTerms
5421+
// Currently just supports search terms separated with one or more spaces
5422+
5423+
// TODO: Add support for more robust search syntax:
5424+
// Search terms wrapped in quotation marks ("") must occur exactly as they appear in the quotes
5425+
// Search terms with an attached minus sign (e.g., -term) are to be excluded from results
5426+
// Boolean AND/OR operators
5427+
// ALSO: Add localization support
5428+
5429+
var thisObj = this;
5430+
5431+
if (this.searchDiv && this.searchString) {
5432+
if ($('#' + this.SearchDiv)) {
5433+
var resultsArray = this.searchFor(this.searchString);
5434+
if (resultsArray.length > 0) {
5435+
var resultsSummary = $('<p>',{
5436+
'class': 'able-search-results-summary'
5437+
});
5438+
var resultsSummaryText = 'Found <strong>' + resultsArray.length + '</strong> matching items. ';
5439+
resultsSummaryText += 'Click the time associated with any item ';
5440+
resultsSummaryText += 'to play the video from that point.';
5441+
resultsSummary.html(resultsSummaryText);
5442+
var resultsList = $('<ul>');
5443+
for (var i in resultsArray) {
5444+
var resultsItem = $('<li>',{
5445+
});
5446+
var itemStartTime = this.secondsToTime(resultsArray[i]['start']);
5447+
var itemStartSpan = $('<span>',{
5448+
'class': 'able-search-results-time',
5449+
'data-start': resultsArray[i]['start'],
5450+
'title': itemStartTime['title'],
5451+
'tabindex': '0'
5452+
});
5453+
itemStartSpan.text(itemStartTime['value']);
5454+
// add a listener for clisk on itemStart
5455+
itemStartSpan.click(function(event) {
5456+
var spanStart = parseFloat($(this).attr('data-start'));
5457+
// Add a tiny amount so that we're inside the span.
5458+
spanStart += .01;
5459+
thisObj.seeking = true;
5460+
thisObj.seekTo(spanStart);
5461+
});
5462+
5463+
var itemText = $('<span>',{
5464+
'class': 'able-search-result-text'
5465+
})
5466+
itemText.html('...' + resultsArray[i]['caption'] + '...');
5467+
resultsItem.append(itemStartSpan, itemText);
5468+
resultsList.append(resultsItem);
5469+
}
5470+
$('#' + this.searchDiv).append(resultsSummary, resultsList);
5471+
}
5472+
else {
5473+
var noResults = $('<p>').text('No results found.');
5474+
$('#' + this.searchDiv).append(noResults);
5475+
}
5476+
}
5477+
}
5478+
};
5479+
5480+
AblePlayer.prototype.searchFor = function(searchString) {
5481+
5482+
// return chronological array of caption cues that match searchTerms
5483+
5484+
var captionLang, captions, results, caption, c, i, j;
5485+
5486+
// split searchTerms into an array
5487+
var searchTerms = searchString.split(' ');
5488+
5489+
if (this.captions.length > 0) {
5490+
captionLang = this.captions[0].language; // in case it's needed later
5491+
captions = this.captions[0].cues;
5492+
if (captions.length > 0) {
5493+
var results = [];
5494+
c = 0;
5495+
for (i in captions) {
5496+
if (captions[i].components.children[0]['type'] === 'string') {
5497+
caption = captions[i].components.children[0]['value'];
5498+
for (j in searchTerms) {
5499+
if (caption.indexOf(searchTerms[j]) !== -1) {
5500+
results[c] = [];
5501+
results[c]['start'] = captions[i].start;
5502+
results[c]['caption'] = this.highlightSearchTerm(searchTerms,j,caption);
5503+
c++;
5504+
break;
5505+
}
5506+
}
5507+
}
5508+
}
5509+
}
5510+
}
5511+
5512+
return results;
5513+
};
5514+
5515+
AblePlayer.prototype.highlightSearchTerm = function(searchTerms, index, resultString) {
5516+
5517+
// highlight ALL found searchTerms in the current resultString
5518+
// index is the first index in the searchTerm array where a match has already been found
5519+
// Need to step through the remaining terms to see if they're present as well
5520+
5521+
var i, searchTerm, termIndex, termLength, str1, str2, str3;
5522+
5523+
for (i=index; i<searchTerms.length; i++) {
5524+
5525+
searchTerm = searchTerms[i];
5526+
termIndex = resultString.indexOf(searchTerm);
5527+
if (termIndex !== -1) {
5528+
termLength = searchTerm.length;
5529+
if (termLength > 0) {
5530+
str1 = resultString.substring(0, termIndex);
5531+
str2 = '<span class="able-search-term">' + searchTerm + '</span>';
5532+
str3 = resultString.substring(termIndex+termLength);
5533+
resultString = str1 + str2 + str3;
5534+
}
5535+
else {
5536+
str1 = '<span class="able-search-term">' + searchTerm + '</span>';
5537+
str2 = resultString.substring(termIndex+termLength);
5538+
resultString = str1 + str2;
5539+
}
5540+
}
5541+
}
5542+
return resultString;
5543+
};
5544+
5545+
AblePlayer.prototype.secondsToTime = function(totalSeconds) {
5546+
5547+
// return an array of totalSeconds converted into two formats
5548+
// time['value'] = HH:MM:SS with hours dropped if there are none
5549+
// time['title'] = a speakable rendering, so speech rec users can easily speak the link
5550+
5551+
// first, round down to nearest second
5552+
var totalSeconds = Math.floor(totalSeconds);
5553+
5554+
var hours = parseInt( totalSeconds / 3600 ) % 24;
5555+
var minutes = parseInt( totalSeconds / 60 ) % 60;
5556+
var seconds = totalSeconds % 60;
5557+
var value = '';
5558+
var title = '';
5559+
if (hours > 0) {
5560+
value += hours + ':';
5561+
title + hours + ' hours ';
5562+
}
5563+
if (minutes < 10) {
5564+
value += '0' + minutes + ':';
5565+
if (minutes > 0) {
5566+
title += minutes + ' minutes ';
5567+
}
5568+
}
5569+
else {
5570+
value += minutes + ':';
5571+
title += minutes + ' minutes ';
5572+
}
5573+
if (seconds < 10) {
5574+
value += '0' + seconds;
5575+
if (seconds > 0) {
5576+
title += seconds + ' seconds ';
5577+
}
5578+
}
5579+
else {
5580+
value += seconds;
5581+
title += seconds + ' seconds ';
5582+
}
5583+
var time = [];
5584+
time['value'] = value;
5585+
time['title'] = title;
5586+
return time;
5587+
};
5588+
})();
54085589
(function () {
54095590
// Media events
54105591
AblePlayer.prototype.onMediaUpdateTime = function () {
54115592
if (!this.startedPlaying) {
54125593
if (this.startTime) {
54135594
if (this.startTime === this.media.currentTime) {
54145595
// media has already scrubbed to start time
5415-
if (this.autoplay) {
5596+
if (this.autoplay || this.seeking) {
54165597
this.playMedia();
5417-
}
5598+
this.seeking = false;
5599+
}
54185600
}
54195601
else {
54205602
// continue seeking ahead until currentTime == startTime

compile.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ cat scripts/caption.js >> build/ableplayer.js
1717
cat scripts/metadata.js >> build/ableplayer.js
1818
cat scripts/translation.js >> build/ableplayer.js
1919
cat scripts/transcript.js >> build/ableplayer.js
20+
cat scripts/search.js >> build/ableplayer.js
2021
cat scripts/event.js >> build/ableplayer.js

scripts/ableplayer-base.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,14 @@
177177
this.metaDiv = $(media).data('meta-div');
178178
}
179179

180+
if ($(media).data('search') !== undefined && $(media).data('search') !== "") {
181+
// conducting a search currently requires an external div in which to write the results
182+
if ($(media).data('search-div') !== undefined && $(media).data('search-div') !== "") {
183+
this.searchString = $(media).data('search');
184+
this.searchDiv = $(media).data('search-div');
185+
}
186+
}
187+
180188
this.ableIndex = AblePlayer.nextIndex;
181189
AblePlayer.nextIndex += 1;
182190

scripts/event.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
if (this.startTime) {
66
if (this.startTime === this.media.currentTime) {
77
// media has already scrubbed to start time
8-
if (this.autoplay) {
8+
if (this.autoplay || this.seeking) {
99
this.playMedia();
10-
}
10+
this.seeking = false;
11+
}
1112
}
1213
else {
1314
// continue seeking ahead until currentTime == startTime

scripts/initialize.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@
345345
thisObj.updateDescription();
346346
thisObj.updateCaption();
347347
thisObj.updateTranscript();
348+
thisObj.showSearchResults();
348349
thisObj.initializing = false;
349350
thisObj.refreshControls();
350351

0 commit comments

Comments
 (0)