Files
shaka-player/test/text/text_engine_unit.js
T
Joey Parrish 9cce246325 Fix TextEngine buffered range calculations
This reverts commit c38d4dd8d3, which
actually broke text range calculations in v2.3.10, and v2.4.2-v2.4.4.
The original commit was meant to account for the period start, but
resulted in a double-accounting of presentationTimeOffset.

The start and ends times passed into TextEngine's appendBuffer were
period-relative, so timestampOffset had already been applied.  To
avoid further confusion and to fix the original issue the reverted
commit tried to address, these have been changed to
presentation-relative timestamps.  Now the period start and all
offsets have been accounted for before the metadata reaches
MediaSourceEngine and TextEngine.

The tests added in the bad commit have been modified to test for the
opposite: that we do not erroneously account for timestamp offset when
calculating the buffered ranges for text.

Closes #1562

Change-Id: I9fa7a3f59906c4f3e623f411e48551f86f5c2ff7
2018-08-28 17:21:39 +00:00

364 lines
12 KiB
JavaScript

/**
* @license
* Copyright 2016 Google Inc.
*
* 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.
*/
describe('TextEngine', function() {
const TextEngine = shaka.text.TextEngine;
const dummyData = new ArrayBuffer(0);
const dummyMimeType = 'text/fake';
/** @type {!Function} */
let mockParserPlugIn;
/** @type {!shaka.test.FakeTextDisplayer} */
let mockDisplayer;
/** @type {!jasmine.Spy} */
let mockParseInit;
/** @type {!jasmine.Spy} */
let mockParseMedia;
/** @type {!shaka.text.TextEngine} */
let textEngine;
beforeEach(function() {
mockParseInit = jasmine.createSpy('mockParseInit');
mockParseMedia = jasmine.createSpy('mockParseMedia');
mockParserPlugIn = function() {
return {
parseInit: mockParseInit,
parseMedia: mockParseMedia,
};
};
mockDisplayer = new shaka.test.FakeTextDisplayer();
TextEngine.registerParser(dummyMimeType, mockParserPlugIn);
textEngine = new TextEngine(mockDisplayer);
textEngine.initParser(dummyMimeType);
});
afterEach(function() {
TextEngine.unregisterParser(dummyMimeType);
});
describe('isTypeSupported', function() {
it('reports support only when a parser is installed', function() {
TextEngine.unregisterParser(dummyMimeType);
expect(TextEngine.isTypeSupported(dummyMimeType)).toBe(false);
TextEngine.registerParser(dummyMimeType, mockParserPlugIn);
expect(TextEngine.isTypeSupported(dummyMimeType)).toBe(true);
TextEngine.unregisterParser(dummyMimeType);
expect(TextEngine.isTypeSupported(dummyMimeType)).toBe(false);
});
});
describe('appendBuffer', function() {
it('works asynchronously', function(done) {
mockParseMedia.and.returnValue([1, 2, 3]);
textEngine.appendBuffer(dummyData, 0, 3).catch(fail).then(done);
expect(mockDisplayer.append).not.toHaveBeenCalled();
});
it('calls displayer.append()', function(done) {
let cue1 = createFakeCue(1, 2);
let cue2 = createFakeCue(2, 3);
let cue3 = createFakeCue(3, 4);
let cue4 = createFakeCue(4, 5);
mockParseMedia.and.returnValue([cue1, cue2]);
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
expect(mockParseMedia).toHaveBeenCalledWith(
new Uint8Array(dummyData),
{periodStart: 0, segmentStart: 0, segmentEnd: 3});
expect(mockDisplayer.append).toHaveBeenCalledWith([cue1, cue2]);
expect(mockDisplayer.remove).not.toHaveBeenCalled();
mockDisplayer.append.calls.reset();
mockParseMedia.calls.reset();
mockParseMedia.and.returnValue([cue3, cue4]);
return textEngine.appendBuffer(dummyData, 3, 5);
}).then(function() {
expect(mockParseMedia).toHaveBeenCalledWith(
new Uint8Array(dummyData),
{periodStart: 0, segmentStart: 3, segmentEnd: 5});
expect(mockDisplayer.append).toHaveBeenCalledWith([cue3, cue4]);
}).catch(fail).then(done);
});
it('does not throw if called right before destroy', function(done) {
mockParseMedia.and.returnValue([1, 2, 3]);
textEngine.appendBuffer(dummyData, 0, 3).catch(fail).then(done);
textEngine.destroy();
});
});
describe('remove', function() {
let cue1;
let cue2;
let cue3;
beforeEach(function() {
cue1 = createFakeCue(0, 1);
cue2 = createFakeCue(1, 2);
cue3 = createFakeCue(2, 3);
mockParseMedia.and.returnValue([cue1, cue2, cue3]);
});
it('works asynchronously', function(done) {
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
let p = textEngine.remove(0, 1);
expect(mockDisplayer.remove).not.toHaveBeenCalled();
return p;
}).catch(fail).then(done);
});
it('calls displayer.remove()', function(done) {
textEngine.remove(0, 1).then(function() {
expect(mockDisplayer.remove).toHaveBeenCalledWith(0, 1);
}).catch(fail).then(done);
});
it('does not throw if called right before destroy', function(done) {
textEngine.remove(0, 1).catch(fail).then(done);
textEngine.destroy();
});
});
describe('setTimestampOffset', function() {
it('passes the offset to the parser', function(done) {
mockParseMedia.and.callFake(function(data, time) {
return [
createFakeCue(time.periodStart + 0,
time.periodStart + 1),
createFakeCue(time.periodStart + 2,
time.periodStart + 3),
];
});
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
expect(mockParseMedia).toHaveBeenCalledWith(
new Uint8Array(dummyData),
{periodStart: 0, segmentStart: 0, segmentEnd: 3});
expect(mockDisplayer.append).toHaveBeenCalledWith(
[
createFakeCue(0, 1),
createFakeCue(2, 3),
]);
mockDisplayer.append.calls.reset();
textEngine.setTimestampOffset(4);
return textEngine.appendBuffer(dummyData, 4, 7);
}).then(function() {
expect(mockParseMedia).toHaveBeenCalledWith(
new Uint8Array(dummyData),
{periodStart: 4, segmentStart: 4, segmentEnd: 7});
expect(mockDisplayer.append).toHaveBeenCalledWith(
[
createFakeCue(4, 5),
createFakeCue(6, 7),
]);
}).catch(fail).then(done);
});
});
describe('bufferStart/bufferEnd', function() {
beforeEach(function() {
mockParseMedia.and.callFake(function() {
return [createFakeCue(0, 1), createFakeCue(1, 2), createFakeCue(2, 3)];
});
});
it('return null when there are no cues', function() {
expect(textEngine.bufferStart()).toBe(null);
expect(textEngine.bufferEnd()).toBe(null);
});
it('reflect newly-added cues', function(done) {
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
expect(textEngine.bufferStart()).toBe(0);
expect(textEngine.bufferEnd()).toBe(3);
return textEngine.appendBuffer(dummyData, 3, 6);
}).then(function() {
expect(textEngine.bufferStart()).toBe(0);
expect(textEngine.bufferEnd()).toBe(6);
return textEngine.appendBuffer(dummyData, 6, 10);
}).then(function() {
expect(textEngine.bufferStart()).toBe(0);
expect(textEngine.bufferEnd()).toBe(10);
}).catch(fail).then(done);
});
it('reflect newly-removed cues', function(done) {
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
return textEngine.appendBuffer(dummyData, 3, 6);
}).then(function() {
return textEngine.appendBuffer(dummyData, 6, 10);
}).then(function() {
expect(textEngine.bufferStart()).toBe(0);
expect(textEngine.bufferEnd()).toBe(10);
return textEngine.remove(0, 3);
}).then(function() {
expect(textEngine.bufferStart()).toBe(3);
expect(textEngine.bufferEnd()).toBe(10);
return textEngine.remove(8, 11);
}).then(function() {
expect(textEngine.bufferStart()).toBe(3);
expect(textEngine.bufferEnd()).toBe(8);
return textEngine.remove(11, 20);
}).then(function() {
expect(textEngine.bufferStart()).toBe(3);
expect(textEngine.bufferEnd()).toBe(8);
return textEngine.remove(0, Infinity);
}).then(function() {
expect(textEngine.bufferStart()).toBe(null);
expect(textEngine.bufferEnd()).toBe(null);
}).catch(fail).then(done);
});
it('does not use timestamp offset', async function() {
// The start and end times passed to appendBuffer are now absolute, so
// they already account for timestampOffset and period offset.
// See https://github.com/google/shaka-player/issues/1562
textEngine.setTimestampOffset(60);
await textEngine.appendBuffer(dummyData, 0, 3);
expect(textEngine.bufferStart()).toBe(0);
expect(textEngine.bufferEnd()).toBe(3);
await textEngine.appendBuffer(dummyData, 3, 6);
expect(textEngine.bufferStart()).toBe(0);
expect(textEngine.bufferEnd()).toBe(6);
});
});
describe('bufferedAheadOf', function() {
beforeEach(function() {
mockParseMedia.and.callFake(function() {
return [createFakeCue(0, 1), createFakeCue(1, 2), createFakeCue(2, 3)];
});
});
it('returns 0 when there are no cues', function() {
expect(textEngine.bufferedAheadOf(0)).toBe(0);
});
it('returns 0 if |t| is not buffered', function(done) {
textEngine.appendBuffer(dummyData, 3, 6).then(function() {
expect(textEngine.bufferedAheadOf(6.1)).toBe(0);
}).catch(fail).then(done);
});
it('ignores gaps in the content', function(done) {
textEngine.appendBuffer(dummyData, 3, 6).then(function() {
expect(textEngine.bufferedAheadOf(2)).toBe(3);
}).catch(fail).then(done);
});
it('returns the distance to the end if |t| is buffered', function(done) {
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
expect(textEngine.bufferedAheadOf(0)).toBe(3);
expect(textEngine.bufferedAheadOf(1)).toBe(2);
expect(textEngine.bufferedAheadOf(2.5)).toBeCloseTo(0.5);
}).catch(fail).then(done);
});
it('does not use timestamp offset', async function() {
// The start and end times passed to appendBuffer are now absolute, so
// they already account for timestampOffset and period offset.
// See https://github.com/google/shaka-player/issues/1562
textEngine.setTimestampOffset(60);
await textEngine.appendBuffer(dummyData, 3, 6);
expect(textEngine.bufferedAheadOf(4)).toBe(2);
expect(textEngine.bufferedAheadOf(64)).toBe(0);
});
});
describe('setAppendWindow', function() {
beforeEach(function() {
mockParseMedia.and.callFake(function() {
return [createFakeCue(0, 1), createFakeCue(1, 2), createFakeCue(2, 3)];
});
});
it('limits appended cues', function(done) {
textEngine.setAppendWindow(0, 1.9);
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
expect(mockDisplayer.append).toHaveBeenCalledWith(
[
createFakeCue(0, 1),
createFakeCue(1, 2),
]);
mockDisplayer.append.calls.reset();
textEngine.setAppendWindow(1, 2.1);
return textEngine.appendBuffer(dummyData, 0, 3);
}).then(function() {
expect(mockDisplayer.append).toHaveBeenCalledWith(
[
createFakeCue(1, 2),
createFakeCue(2, 3),
]);
}).catch(fail).then(done);
});
it('limits bufferStart', function(done) {
textEngine.setAppendWindow(1, 9);
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
expect(textEngine.bufferStart()).toBe(1);
return textEngine.remove(0, 9);
}).then(function() {
textEngine.setAppendWindow(2.1, 9);
return textEngine.appendBuffer(dummyData, 0, 3);
}).then(function() {
expect(textEngine.bufferStart()).toBe(2.1);
}).catch(fail).then(done);
});
it('limits bufferEnd', function(done) {
textEngine.setAppendWindow(0, 1.9);
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
expect(textEngine.bufferEnd()).toBe(1.9);
textEngine.setAppendWindow(0, 2.1);
return textEngine.appendBuffer(dummyData, 0, 3);
}).then(function() {
expect(textEngine.bufferEnd()).toBe(2.1);
textEngine.setAppendWindow(0, 4.1);
return textEngine.appendBuffer(dummyData, 0, 3);
}).then(function() {
expect(textEngine.bufferEnd()).toBe(3);
}).catch(fail).then(done);
});
});
function createFakeCue(startTime, endTime) {
return {startTime: startTime, endTime: endTime};
}
});