blob: fa0636ab1f0818fa7d53842d88774d269776903c [file] [log] [blame]
<!DOCTYPE html>
<html>
<head>
<script src="/w3c/resources/testharness.js"></script>
<script src="/w3c/resources/testharnessreport.js"></script>
<script src="mediasource-util.js"></script>
<link rel='stylesheet' href='/w3c/resources/testharness.css'>
</head>
<body>
<div id="log"></div>
<script>
mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
{
test.expectEvent(sourceBuffer, 'updatestart', 'Append started.');
test.expectEvent(sourceBuffer, 'update', 'Append success.');
test.expectEvent(sourceBuffer, 'updateend', 'Append ended.');
sourceBuffer.appendBuffer(mediaData);
assert_true(sourceBuffer.updating, 'updating attribute is true');
test.waitForExpectedEvents(function()
{
assert_false(sourceBuffer.updating, 'updating attribute is false');
test.done();
});
}, 'Test SourceBuffer.appendBuffer() event dispatching.');
mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
{
test.expectEvent(sourceBuffer, 'updatestart', 'Append started.');
test.expectEvent(sourceBuffer, 'update', 'Append success.');
test.expectEvent(sourceBuffer, 'updateend', 'Append ended.');
sourceBuffer.appendBuffer(mediaData);
assert_true(sourceBuffer.updating, 'updating attribute is true');
assert_throws_dom('InvalidStateError',
function() { sourceBuffer.appendBuffer(mediaData); },
'appendBuffer() throws an exception there is a pending append.');
assert_true(sourceBuffer.updating, 'updating attribute is true');
test.waitForExpectedEvents(function()
{
assert_false(sourceBuffer.updating, 'updating attribute is false');
test.done();
});
}, 'Test SourceBuffer.appendBuffer() call during a pending appendBuffer().');
mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
{
test.expectEvent(sourceBuffer, 'updatestart', 'Append started.');
test.expectEvent(sourceBuffer, 'abort', 'Append aborted.');
test.expectEvent(sourceBuffer, 'updateend', 'Append ended.');
sourceBuffer.appendBuffer(mediaData);
assert_true(sourceBuffer.updating, 'updating attribute is true');
sourceBuffer.abort();
assert_false(sourceBuffer.updating, 'updating attribute is false');
test.waitForExpectedEvents(function()
{
assert_false(sourceBuffer.updating, 'updating attribute is false');
test.done();
});
}, 'Test SourceBuffer.abort() call during a pending appendBuffer().');
mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
{
test.expectEvent(sourceBuffer, 'updatestart', 'Append started.');
test.expectEvent(sourceBuffer, 'update', 'Append success.');
test.expectEvent(sourceBuffer, 'updateend', 'Append ended.');
sourceBuffer.appendBuffer(mediaData);
assert_true(sourceBuffer.updating, 'updating attribute is true');
test.waitForExpectedEvents(function()
{
assert_false(sourceBuffer.updating, 'updating attribute is false');
test.expectEvent(mediaSource, 'sourceended', 'MediaSource sourceended event');
mediaSource.endOfStream();
assert_equals(mediaSource.readyState, 'ended', 'MediaSource readyState is "ended"');
});
test.waitForExpectedEvents(function()
{
assert_equals(mediaSource.readyState, 'ended', 'MediaSource readyState is "ended"');
test.expectEvent(mediaSource, 'sourceopen', 'MediaSource sourceopen event');
test.expectEvent(sourceBuffer, 'updatestart', 'Append started.');
test.expectEvent(sourceBuffer, 'update', 'Append success.');
test.expectEvent(sourceBuffer, 'updateend', 'Append ended.');
sourceBuffer.appendBuffer(mediaData);
assert_equals(mediaSource.readyState, 'open', 'MediaSource readyState is "open"');
assert_true(sourceBuffer.updating, 'updating attribute is true');
});
test.waitForExpectedEvents(function()
{
assert_equals(mediaSource.readyState, 'open', 'MediaSource readyState is "open"');
assert_false(sourceBuffer.updating, 'updating attribute is false');
test.done();
});
}, 'Test SourceBuffer.appendBuffer() triggering an "ended" to "open" transition.');
mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
{
test.expectEvent(sourceBuffer, 'updatestart', 'Append started.');
test.expectEvent(sourceBuffer, 'update', 'Append success.');
test.expectEvent(sourceBuffer, 'updateend', 'Append ended.');
sourceBuffer.appendBuffer(mediaData);
assert_true(sourceBuffer.updating, 'updating attribute is true');
test.waitForExpectedEvents(function()
{
assert_false(sourceBuffer.updating, 'updating attribute is false');
test.expectEvent(mediaSource, 'sourceended', 'MediaSource sourceended event');
mediaSource.endOfStream();
assert_equals(mediaSource.readyState, 'ended', 'MediaSource readyState is "ended"');
});
test.waitForExpectedEvents(function()
{
assert_equals(mediaSource.readyState, 'ended', 'MediaSource readyState is "ended"');
test.expectEvent(mediaSource, 'sourceopen', 'MediaSource sourceopen event');
test.expectEvent(sourceBuffer, 'updatestart', 'Append started.');
test.expectEvent(sourceBuffer, 'update', 'Append success.');
test.expectEvent(sourceBuffer, 'updateend', 'Append ended.');
sourceBuffer.appendBuffer(new Uint8Array(0));
assert_equals(mediaSource.readyState, 'open', 'MediaSource readyState is "open"');
assert_true(sourceBuffer.updating, 'updating attribute is true');
});
test.waitForExpectedEvents(function()
{
assert_equals(mediaSource.readyState, 'open', 'MediaSource readyState is "open"');
assert_false(sourceBuffer.updating, 'updating attribute is false');
test.done();
});
}, 'Test zero byte SourceBuffer.appendBuffer() call triggering an "ended" to "open" transition.');
mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
{
test.expectEvent(sourceBuffer, 'updatestart', 'Append started.');
test.expectEvent(sourceBuffer, 'abort', 'Append aborted.');
test.expectEvent(sourceBuffer, 'updateend', 'Append ended.');
sourceBuffer.appendBuffer(mediaData);
assert_true(sourceBuffer.updating, 'updating attribute is true');
assert_equals(mediaSource.activeSourceBuffers.length, 0, 'activeSourceBuffers.length');
test.expectEvent(mediaSource.sourceBuffers, 'removesourcebuffer', 'sourceBuffers');
mediaSource.removeSourceBuffer(sourceBuffer);
assert_false(sourceBuffer.updating, 'updating attribute is false');
assert_throws_dom('InvalidStateError',
function() { sourceBuffer.appendBuffer(mediaData); },
'appendBuffer() throws an exception because it isn\'t attached to the mediaSource anymore.');
test.waitForExpectedEvents(function()
{
assert_false(sourceBuffer.updating, 'updating attribute is false');
test.done();
});
}, 'Test MediaSource.removeSourceBuffer() call during a pending appendBuffer().');
mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
{
test.expectEvent(sourceBuffer, 'updatestart', 'Append started.');
test.expectEvent(sourceBuffer, 'updateend', 'Append ended.');
sourceBuffer.appendBuffer(mediaData);
assert_true(sourceBuffer.updating, 'updating attribute is true');
assert_throws_dom('InvalidStateError',
function() { mediaSource.duration = 1.0; },
'set duration throws an exception when updating attribute is true.');
test.waitForExpectedEvents(function()
{
assert_false(sourceBuffer.updating, 'updating attribute is false');
test.done();
});
}, 'Test set MediaSource.duration during a pending appendBuffer() for one of its SourceBuffers.');
mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
{
test.failOnEvent(mediaSource, 'sourceended');
test.expectEvent(sourceBuffer, 'updatestart', 'Append started.');
test.expectEvent(sourceBuffer, 'updateend', 'Append ended.');
sourceBuffer.appendBuffer(mediaData);
assert_true(sourceBuffer.updating, 'updating attribute is true');
assert_throws_dom('InvalidStateError',
function() { mediaSource.endOfStream(); },
'endOfStream() throws an exception when updating attribute is true.');
assert_equals(mediaSource.readyState, 'open');
test.waitForExpectedEvents(function()
{
assert_false(sourceBuffer.updating, 'updating attribute is false');
assert_equals(mediaSource.readyState, 'open');
test.done();
});
}, 'Test MediaSource.endOfStream() during a pending appendBuffer() for one of its SourceBuffers.');
mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
{
test.expectEvent(sourceBuffer, 'updatestart', 'Append started.');
test.expectEvent(sourceBuffer, 'updateend', 'Append ended.');
sourceBuffer.appendBuffer(mediaData);
assert_true(sourceBuffer.updating, 'updating attribute is true');
assert_throws_dom('InvalidStateError',
function() { sourceBuffer.timestampOffset = 10.0; },
'set timestampOffset throws an exception when updating attribute is true.');
test.waitForExpectedEvents(function()
{
assert_false(sourceBuffer.updating, 'updating attribute is false');
test.done();
});
}, 'Test set SourceBuffer.timestampOffset during a pending appendBuffer().');
mediasource_test(function(test, mediaElement, mediaSource)
{
var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.VIDEO_ONLY_TYPE);
test.expectEvent(sourceBuffer, 'updatestart', 'Append started.');
test.expectEvent(sourceBuffer, 'update', 'Append success.');
test.expectEvent(sourceBuffer, 'updateend', 'Append ended.');
sourceBuffer.appendBuffer(new Uint8Array(0));
assert_true(sourceBuffer.updating, 'updating attribute is true');
test.waitForExpectedEvents(function()
{
assert_false(sourceBuffer.updating, 'updating attribute is false');
test.done();
});
}, 'Test appending an empty ArrayBufferView.');
mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
{
test.expectEvent(sourceBuffer, 'updatestart', 'Append started.');
test.expectEvent(sourceBuffer, 'update', 'Append success.');
test.expectEvent(sourceBuffer, 'updateend', 'Append ended.');
var arrayBufferView = new Uint8Array(mediaData);
assert_equals(arrayBufferView.length, mediaData.length, 'arrayBufferView.length before transfer.');
// Send the buffer as in a message so it gets neutered.
window.postMessage( 'test', '*', [arrayBufferView.buffer]);
assert_equals(arrayBufferView.length, 0, 'arrayBufferView.length after transfer.');
sourceBuffer.appendBuffer(arrayBufferView);
assert_true(sourceBuffer.updating, 'updating attribute is true');
test.waitForExpectedEvents(function()
{
assert_false(sourceBuffer.updating, 'updating attribute is false');
test.done();
});
}, 'Test appending a neutered ArrayBufferView.');
mediasource_test(function(test, mediaElement, mediaSource)
{
var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.VIDEO_ONLY_TYPE);
test.expectEvent(sourceBuffer, 'updatestart', 'Append started.');
test.expectEvent(sourceBuffer, 'update', 'Append success.');
test.expectEvent(sourceBuffer, 'updateend', 'Append ended.');
sourceBuffer.appendBuffer(new ArrayBuffer(0));
assert_true(sourceBuffer.updating, 'updating attribute is true');
test.waitForExpectedEvents(function()
{
assert_false(sourceBuffer.updating, 'updating attribute is false');
test.done();
});
}, 'Test appending an empty ArrayBuffer.');
mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
{
test.expectEvent(sourceBuffer, 'updatestart', 'Append started.');
test.expectEvent(sourceBuffer, 'update', 'Append success.');
test.expectEvent(sourceBuffer, 'updateend', 'Append ended.');
var arrayBuffer = mediaData.buffer.slice();
assert_equals(arrayBuffer.byteLength, mediaData.buffer.byteLength, 'arrayBuffer.byteLength before transfer.');
// Send the buffer as in a message so it gets neutered.
window.postMessage( 'test', '*', [arrayBuffer]);
assert_equals(arrayBuffer.byteLength, 0, 'arrayBuffer.byteLength after transfer.');
sourceBuffer.appendBuffer(arrayBuffer);
assert_true(sourceBuffer.updating, 'updating attribute is true');
test.waitForExpectedEvents(function()
{
assert_false(sourceBuffer.updating, 'updating attribute is false');
test.done();
});
}, 'Test appending a neutered ArrayBuffer.');
mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
{
var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init);
var halfIndex = (initSegment.length - 1) / 2;
var partialInitSegment = initSegment.subarray(0, halfIndex);
var remainingInitSegment = initSegment.subarray(halfIndex);
var mediaSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]);
test.expectEvent(sourceBuffer, 'updateend', 'partialInitSegment append ended.');
sourceBuffer.appendBuffer(partialInitSegment);
test.waitForExpectedEvents(function()
{
assert_equals(mediaElement.readyState, mediaElement.HAVE_NOTHING);
assert_equals(mediaSource.duration, Number.NaN);
test.expectEvent(sourceBuffer, 'updateend', 'remainingInitSegment append ended.');
test.expectEvent(mediaElement, 'loadedmetadata', 'loadedmetadata event received.');
sourceBuffer.appendBuffer(remainingInitSegment);
});
test.waitForExpectedEvents(function()
{
assert_equals(mediaElement.readyState, mediaElement.HAVE_METADATA);
assert_equals(mediaSource.duration, segmentInfo.durationInInitSegment);
test.expectEvent(sourceBuffer, 'updateend', 'mediaSegment append ended.');
test.expectEvent(mediaElement, 'loadeddata', 'loadeddata fired.');
sourceBuffer.appendBuffer(mediaSegment);
});
test.waitForExpectedEvents(function()
{
assert_greater_than_equal(mediaElement.readyState, mediaElement.HAVE_CURRENT_DATA);
assert_equals(sourceBuffer.updating, false);
assert_equals(mediaSource.readyState, 'open');
test.done();
});
}, 'Test appendBuffer with partial init segments.');
mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
{
var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init);
var mediaSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]);
var halfIndex = (mediaSegment.length - 1) / 2;
var partialMediaSegment = mediaSegment.subarray(0, halfIndex);
var remainingMediaSegment = mediaSegment.subarray(halfIndex);
test.expectEvent(sourceBuffer, 'updateend', 'InitSegment append ended.');
test.expectEvent(mediaElement, 'loadedmetadata', 'loadedmetadata done.');
sourceBuffer.appendBuffer(initSegment);
test.waitForExpectedEvents(function()
{
assert_equals(mediaElement.readyState, mediaElement.HAVE_METADATA);
assert_equals(mediaSource.duration, segmentInfo.durationInInitSegment);
test.expectEvent(sourceBuffer, 'updateend', 'partial media segment append ended.');
sourceBuffer.appendBuffer(partialMediaSegment);
});
test.waitForExpectedEvents(function()
{
test.expectEvent(sourceBuffer, 'updateend', 'mediaSegment append ended.');
test.expectEvent(mediaElement, 'loadeddata', 'loadeddata fired.');
sourceBuffer.appendBuffer(remainingMediaSegment);
});
test.waitForExpectedEvents(function()
{
assert_greater_than_equal(mediaElement.readyState, mediaElement.HAVE_CURRENT_DATA);
assert_equals(mediaSource.readyState, 'open');
assert_equals(sourceBuffer.updating, false);
test.done();
});
}, 'Test appendBuffer with partial media segments.');
mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
{
var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init);
var partialInitSegment = initSegment.subarray(0, initSegment.length / 2);
var mediaSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]);
test.expectEvent(sourceBuffer, 'updateend', 'partialInitSegment append ended.');
sourceBuffer.appendBuffer(partialInitSegment);
test.waitForExpectedEvents(function()
{
// Call abort to reset the parser.
sourceBuffer.abort();
// Append the full intiialization segment.
test.expectEvent(sourceBuffer, 'updateend', 'initSegment append ended.');
sourceBuffer.appendBuffer(initSegment);
});
test.waitForExpectedEvents(function()
{
test.expectEvent(sourceBuffer, 'updateend', 'mediaSegment append ended.');
test.expectEvent(mediaElement, 'loadeddata', 'loadeddata fired.');
sourceBuffer.appendBuffer(mediaSegment);
});
test.waitForExpectedEvents(function()
{
test.done();
});
}, 'Test abort in the middle of an initialization segment.');
mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
{
var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init);
var mediaSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]);
var partialMediaSegment = mediaSegment.subarray(0, 3 * mediaSegment.length / 4);
var partialBufferedRanges = null;
test.expectEvent(sourceBuffer, 'updateend', 'initSegment append ended.');
sourceBuffer.appendBuffer(initSegment);
test.waitForExpectedEvents(function()
{
test.expectEvent(sourceBuffer, 'updateend', 'partialMediaSegment append ended.');
sourceBuffer.appendBuffer(partialMediaSegment);
});
test.waitForExpectedEvents(function()
{
// Call abort to reset the parser.
sourceBuffer.abort();
// Keep a copy of the buffered ranges before we append the whole media segment.
partialBufferedRanges = sourceBuffer.buffered;
assert_equals(partialBufferedRanges.length, 1, 'partialBufferedRanges has 1 range');
// Append the full media segment.
test.expectEvent(sourceBuffer, 'updateend', 'mediaSegment append ended.');
sourceBuffer.appendBuffer(mediaSegment);
});
test.waitForExpectedEvents(function()
{
test.expectEvent(sourceBuffer, 'updateend', 'Append ended.');
assert_equals(sourceBuffer.buffered.length, 1, 'sourceBuffer.buffered has 1 range');
assert_equals(sourceBuffer.buffered.length, partialBufferedRanges.length, 'sourceBuffer.buffered and partialBufferedRanges are the same length.');
assert_equals(sourceBuffer.buffered.start(0), partialBufferedRanges.start(0), 'sourceBuffer.buffered and partialBufferedRanges start times match.');
assert_greater_than(sourceBuffer.buffered.end(0), partialBufferedRanges.end(0), 'sourceBuffer.buffered has a higher end time than partialBufferedRanges.');
test.done();
});
}, 'Test abort in the middle of a media segment.');
mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
{
test.expectEvent(mediaSource.sourceBuffers, 'removesourcebuffer', 'SourceBuffer removed.');
mediaSource.removeSourceBuffer(sourceBuffer);
test.waitForExpectedEvents(function()
{
assert_throws_dom('InvalidStateError',
function() { sourceBuffer.abort(); },
'sourceBuffer.abort() throws an exception for InvalidStateError.');
test.done();
});
}, 'Test abort after removing sourcebuffer.');
mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
{
var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init);
var mediaSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]);
test.expectEvent(sourceBuffer, 'updateend', 'initSegment append ended.');
sourceBuffer.appendBuffer(initSegment);
test.waitForExpectedEvents(function()
{
assert_equals(mediaSource.readyState, 'open', 'readyState is open after init segment appended.');
test.expectEvent(sourceBuffer, 'updateend', 'mediaSegment append ended.');
sourceBuffer.appendBuffer(mediaSegment);
});
test.waitForExpectedEvents(function()
{
assert_equals(sourceBuffer.buffered.length, 1, 'sourceBuffer has a buffered range');
assert_equals(mediaSource.readyState, 'open', 'readyState is open after media segment appended.');
test.expectEvent(mediaSource, 'sourceended', 'source ended');
mediaSource.endOfStream();
});
test.waitForExpectedEvents(function()
{
assert_equals(mediaSource.readyState, 'ended', 'readyState is ended.');
assert_throws_dom('InvalidStateError',
function() { sourceBuffer.abort(); },
'sourceBuffer.abort() throws an exception for InvalidStateError.');
test.done();
});
}, 'Test abort after readyState is ended following init segment and media segment.');
mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
{
test.expectEvent(sourceBuffer, 'updatestart', 'Append started.');
test.expectEvent(sourceBuffer, 'updateend', 'Append ended.');
sourceBuffer.appendWindowStart = 1;
sourceBuffer.appendWindowEnd = 100;
sourceBuffer.appendBuffer(mediaData);
test.waitForExpectedEvents(function()
{
assert_false(sourceBuffer.updating, 'updating attribute is false');
sourceBuffer.abort();
assert_equals(sourceBuffer.appendWindowStart, 0, 'appendWindowStart is reset to 0');
assert_equals(sourceBuffer.appendWindowEnd, Number.POSITIVE_INFINITY,
'appendWindowEnd is reset to +INFINITY');
test.done();
});
}, 'Test abort after appendBuffer update ends.');
mediasource_test(function(test, mediaElement, mediaSource)
{
var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.VIDEO_ONLY_TYPE);
test.expectEvent(sourceBuffer, 'updatestart', 'Append started.');
test.expectEvent(sourceBuffer, 'update', 'Append success.');
test.expectEvent(sourceBuffer, 'updateend', 'Append ended.');
assert_throws_js( TypeError,
function() { sourceBuffer.appendBuffer(null); },
'appendBuffer(null) throws an exception.');
test.done();
}, 'Test appending null.');
if (window.SharedArrayBuffer) {
mediasource_test(function(test, mediaElement, mediaSource)
{
var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.VIDEO_ONLY_TYPE);
test.expectEvent(sourceBuffer, 'updatestart', 'Append started.');
test.expectEvent(sourceBuffer, 'update', 'Append success.');
test.expectEvent(sourceBuffer, 'updateend', 'Append ended.');
assert_throws_js( TypeError,
function() { sourceBuffer.appendBuffer(new Uint8Array(new SharedArrayBuffer(16))); },
'appendBuffer() of SharedArrayBuffer view throws an exception.');
test.done();
}, 'Test appending SharedArrayBuffer view.');
}
mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
{
mediaSource.removeSourceBuffer(sourceBuffer);
assert_throws_dom( 'InvalidStateError',
function() { sourceBuffer.appendBuffer(mediaData); },
'appendBuffer() throws an exception when called after removeSourceBuffer().');
test.done();
}, 'Test appending after removeSourceBuffer().');
</script>
</body>
</html>