Commit b992ee1f authored by Dele Olajide's avatar Dele Olajide Committed by dele

Jitsi Videobridge - Further work on ver 1.3.0

git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/trunk@13991 b35dd754-fafc-0310-a699-88a17e54d16e
parent a1c233d8
...@@ -4,12 +4,12 @@ var connection = null; ...@@ -4,12 +4,12 @@ var connection = null;
var focus = null; var focus = null;
var activecall = null; var activecall = null;
var RTC = null; var RTC = null;
var RTCPeerConnection = null;
var nickname = null; var nickname = null;
var sharedKey = ''; var sharedKey = '';
var roomUrl = null; var roomUrl = null;
var ssrc2jid = {}; var ssrc2jid = {};
var localVideoSrc = null; var localVideoSrc = null;
var flipXLocalVideo = true;
var preziPlayer = null; var preziPlayer = null;
/* window.onbeforeunload = closePageWarning; */ /* window.onbeforeunload = closePageWarning; */
...@@ -23,7 +23,6 @@ function init() { ...@@ -23,7 +23,6 @@ function init() {
window.location.href = 'chromeonly.html'; window.location.href = 'chromeonly.html';
return; return;
} }
RTCPeerconnection = TraceablePeerConnection;
connection = new Strophe.Connection(document.getElementById('boshURL').value || config.bosh || '/http-bind'); connection = new Strophe.Connection(document.getElementById('boshURL').value || config.bosh || '/http-bind');
...@@ -49,11 +48,10 @@ function init() { ...@@ -49,11 +48,10 @@ function init() {
if (config.useStunTurn) { if (config.useStunTurn) {
connection.jingle.getStunAndTurnCredentials(); connection.jingle.getStunAndTurnCredentials();
} }
if (RTC.browser === 'firefox') { getUserMediaWithConstraints( ['audio'], audioStreamReady,
getUserMediaWithConstraints(['audio']); function(error){
} else { console.error('failed to obtain audio stream - stop', error);
getUserMediaWithConstraints(['audio', 'video'], config.resolution || '360'); });
}
document.getElementById('connect').disabled = true; document.getElementById('connect').disabled = true;
} else { } else {
console.log('status', status); console.log('status', status);
...@@ -61,6 +59,31 @@ function init() { ...@@ -61,6 +59,31 @@ function init() {
}); });
} }
function audioStreamReady(stream) {
change_local_audio(stream);
if(RTC.browser !== 'firefox') {
getUserMediaWithConstraints( ['video'], videoStreamReady, videoStreamFailed, config.resolution || '360' );
} else {
doJoin();
}
}
function videoStreamReady(stream) {
change_local_video(stream, true);
doJoin();
}
function videoStreamFailed(error) {
console.warn("Failed to obtain video stream - continue anyway", error);
doJoin();
}
function doJoin() { function doJoin() {
var roomnode = null; var roomnode = null;
var path = window.location.pathname; var path = window.location.pathname;
...@@ -104,39 +127,57 @@ function doJoin() { ...@@ -104,39 +127,57 @@ function doJoin() {
connection.emuc.doJoin(roomjid); connection.emuc.doJoin(roomjid);
} }
$(document).bind('mediaready.jingle', function (event, stream) { function change_local_audio(stream) {
connection.jingle.localStream = stream;
RTC.attachMediaStream($('#localVideo'), stream);
document.getElementById('localVideo').autoplay = true;
document.getElementById('localVideo').volume = 0;
localVideoSrc = document.getElementById('localVideo').src; connection.jingle.localAudio = stream;
updateLargeVideo(localVideoSrc, true, 0); RTC.attachMediaStream($('#localAudio'), stream);
document.getElementById('localAudio').autoplay = true;
document.getElementById('localAudio').volume = 0;
}
$('#localVideo').click(function () { function change_local_video(stream, flipX) {
$(document).trigger("video.selected", [false]);
updateLargeVideo($(this).attr('src'), true, 0);
$('video').each(function (idx, el) { connection.jingle.localVideo = stream;
if (el.id.indexOf('mixedmslabel') !== -1) {
el.volume = 0;
el.volume = 1;
}
});
});
doJoin(); var localVideo = document.createElement('video');
}); localVideo.id = 'localVideo_'+stream.id;
localVideo.autoplay = true;
localVideo.volume = 0; // is it required if audio is separated ?
localVideo.oncontextmenu = function () { return false; };
$(document).bind('mediafailure.jingle', function () { var localVideoContainer = document.getElementById('localVideoContainer');
// FIXME localVideoContainer.appendChild(localVideo);
});
var localVideoSelector = $('#' + localVideo.id);
// Add click handler
localVideoSelector.click(function () { handleVideoThumbClicked(localVideo.src); } );
// Add stream ended handler
stream.onended = function () {
localVideoContainer.removeChild(localVideo);
checkChangeLargeVideo(localVideo.src);
};
// Flip video x axis if needed
flipXLocalVideo = flipX;
if(flipX) {
localVideoSelector.addClass("flipVideoX");
}
// Attach WebRTC stream
RTC.attachMediaStream(localVideoSelector, stream);
localVideoSrc = localVideo.src;
updateLargeVideo(localVideoSrc, 0);
}
$(document).bind('remotestreamadded.jingle', function (event, data, sid) { $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
function waitForRemoteVideo(selector, sid) { function waitForRemoteVideo(selector, sid) {
if(selector.removed) {
console.warn("media removed before had started", selector);
return;
}
var sess = connection.jingle.sessions[sid]; var sess = connection.jingle.sessions[sid];
if (data.stream.id === 'mixedmslabel') return; if (data.stream.id === 'mixedmslabel') return;
videoTracks = data.stream.getVideoTracks(); videoTracks = data.stream.getVideoTracks();
console.log("waiting..", videoTracks, selector[0]);
if (videoTracks.length === 0 || selector[0].currentTime > 0) { if (videoTracks.length === 0 || selector[0].currentTime > 0) {
RTC.attachMediaStream(selector, data.stream); // FIXME: why do i have to do this for FF? RTC.attachMediaStream(selector, data.stream); // FIXME: why do i have to do this for FF?
$(document).trigger('callactive.jingle', [selector, sid]); $(document).trigger('callactive.jingle', [selector, sid]);
...@@ -180,7 +221,9 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) { ...@@ -180,7 +221,9 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
} }
} else { } else {
if (data.stream.id !== 'mixedmslabel') { if (data.stream.id !== 'mixedmslabel') {
console.warn('can not associate stream', data.stream.id, 'with a participant'); console.error('can not associate stream', data.stream.id, 'with a participant');
// We don't want to add it here since it will cause troubles
return;
} }
// FIXME: for the mixed ms we dont need a video -- currently // FIXME: for the mixed ms we dont need a video -- currently
container = document.createElement('span'); container = document.createElement('span');
...@@ -188,8 +231,11 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) { ...@@ -188,8 +231,11 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
remotes.appendChild(container); remotes.appendChild(container);
Util.playSoundNotification('userJoined'); Util.playSoundNotification('userJoined');
} }
var vid = document.createElement('video');
var id = 'remoteVideo_' + sid + '_' + data.stream.id; var isVideo = data.stream.getVideoTracks().length > 0;
var vid = isVideo ? document.createElement('video') : document.createElement('audio');
var id = (isVideo ? 'remoteVideo_' : 'remoteAudio_') + sid + '_' + data.stream.id;
vid.id = id; vid.id = id;
vid.autoplay = true; vid.autoplay = true;
vid.oncontextmenu = function () { return false; }; vid.oncontextmenu = function () { return false; };
...@@ -203,44 +249,85 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) { ...@@ -203,44 +249,85 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
var sel = $('#' + id); var sel = $('#' + id);
sel.hide(); sel.hide();
RTC.attachMediaStream(sel, data.stream); RTC.attachMediaStream(sel, data.stream);
if(isVideo) {
waitForRemoteVideo(sel, sid); waitForRemoteVideo(sel, sid);
}
data.stream.onended = function () { data.stream.onended = function () {
console.log('stream ended', this.id); console.log('stream ended', this.id);
var src = $('#' + id).attr('src');
if (src === $('#largeVideo').attr('src')) {
// this is currently displayed as large
// pick the last visible video in the row
// if nobody else is left, this picks the local video
var pick = $('#remoteVideos>span[id!="mixedstream"]:visible:last>video').get(0);
// mute if localvideo
var isLocalVideo = false;
if (pick) {
if (pick.src === localVideoSrc)
isLocalVideo = true;
updateLargeVideo(pick.src, isLocalVideo, pick.volume); // Mark video as removed to cancel waiting loop(if video is removed before has started)
} sel.removed = true;
} sel.remove();
$('#' + id).parent().remove();
var audioCount = $('#'+container.id+'>audio').length;
var videoCount = $('#'+container.id+'>video').length;
if(!audioCount && !videoCount) {
console.log("Remove whole user");
// Remove whole container
container.remove();
Util.playSoundNotification('userLeft'); Util.playSoundNotification('userLeft');
resizeThumbnails(); resizeThumbnails();
};
sel.click(
function () {
$(document).trigger("video.selected", [false]);
updateLargeVideo($(this).attr('src'), false, 1);
} }
);
checkChangeLargeVideo(vid.src);
};
// Add click handler
sel.click(function () { handleVideoThumbClicked(vid.src); });
// an attempt to work around https://github.com/jitsi/jitmeet/issues/32 // an attempt to work around https://github.com/jitsi/jitmeet/issues/32
if (data.peerjid && sess.peerjid === data.peerjid && if (isVideo
&& data.peerjid && sess.peerjid === data.peerjid &&
data.stream.getVideoTracks().length === 0 && data.stream.getVideoTracks().length === 0 &&
connection.jingle.localStream.getVideoTracks().length > 0) { connection.jingle.localVideo.getVideoTracks().length > 0) {
window.setTimeout(function() { window.setTimeout(function() {
sendKeyframe(sess.peerconnection); sendKeyframe(sess.peerconnection);
}, 3000); }, 3000);
} }
}); });
function handleVideoThumbClicked(videoSrc) {
$(document).trigger("video.selected", [false]);
updateLargeVideo(videoSrc, 1);
$('audio').each(function (idx, el) {
// We no longer mix so we check for local audio now
if(el.id != 'localAudio') {
el.volume = 0;
el.volume = 1;
}
});
}
/**
* Checks if removed video is currently displayed and tries to display another one instead.
* @param removedVideoSrc src stream identifier of the video.
*/
function checkChangeLargeVideo(removedVideoSrc){
if (removedVideoSrc === $('#largeVideo').attr('src')) {
// this is currently displayed as large
// pick the last visible video in the row
// if nobody else is left, this picks the local video
var pick = $('#remoteVideos>span[id!="mixedstream"]:visible:last>video').get(0);
if(!pick) {
console.info("Last visible video no longer exists");
pick = $('#remoteVideos>span[id!="mixedstream"]>video').get(0);
}
// mute if localvideo
if (pick) {
updateLargeVideo(pick.src, pick.volume);
} else {
console.warn("Failed to elect large video");
}
}
}
// an attempt to work around https://github.com/jitsi/jitmeet/issues/32 // an attempt to work around https://github.com/jitsi/jitmeet/issues/32
function sendKeyframe(pc) { function sendKeyframe(pc) {
console.log('sendkeyframe', pc.iceConnectionState); console.log('sendkeyframe', pc.iceConnectionState);
...@@ -355,7 +442,7 @@ $(document).bind('callactive.jingle', function (event, videoelem, sid) { ...@@ -355,7 +442,7 @@ $(document).bind('callactive.jingle', function (event, videoelem, sid) {
videoelem.show(); videoelem.show();
resizeThumbnails(); resizeThumbnails();
updateLargeVideo(videoelem.attr('src'), false, 1); updateLargeVideo(videoelem.attr('src'), 1);
showFocusIndicator(); showFocusIndicator();
} }
...@@ -363,6 +450,8 @@ $(document).bind('callactive.jingle', function (event, videoelem, sid) { ...@@ -363,6 +450,8 @@ $(document).bind('callactive.jingle', function (event, videoelem, sid) {
$(document).bind('callterminated.jingle', function (event, sid, reason) { $(document).bind('callterminated.jingle', function (event, sid, reason) {
// FIXME // FIXME
focus = null;
activecall = null;
}); });
$(document).bind('setLocalDescription.jingle', function (event, sid) { $(document).bind('setLocalDescription.jingle', function (event, sid) {
...@@ -379,15 +468,27 @@ $(document).bind('setLocalDescription.jingle', function (event, sid) { ...@@ -379,15 +468,27 @@ $(document).bind('setLocalDescription.jingle', function (event, sid) {
var ssrc = SDPUtil.find_line(media, 'a=ssrc:').substring(7).split(' ')[0]; var ssrc = SDPUtil.find_line(media, 'a=ssrc:').substring(7).split(' ')[0];
newssrcs[type] = ssrc; newssrcs[type] = ssrc;
directions[type] = (SDPUtil.find_line(media, 'a=sendrecv') || SDPUtil.find_line(media, 'a=recvonly') || SDPUtil.find_line('a=sendonly') || SDPUtil.find_line('a=inactive') || 'a=sendrecv').substr(2); directions[type] = (
SDPUtil.find_line(media, 'a=sendrecv')
|| SDPUtil.find_line(media, 'a=recvonly')
|| SDPUtil.find_line('a=sendonly')
|| SDPUtil.find_line('a=inactive')
|| 'a=sendrecv' ).substr(2);
} }
}); });
console.log('new ssrcs', newssrcs); console.log('new ssrcs', newssrcs);
// Have to clear presence map to get rid of removed streams
connection.emuc.clearPresenceMedia();
var i = 0; var i = 0;
Object.keys(newssrcs).forEach(function (mtype) { Object.keys(newssrcs).forEach(function (mtype) {
i++; i++;
connection.emuc.addMediaToPresence(i, mtype, newssrcs[mtype], directions[mtype]); var type = mtype;
// Change video type to screen
if(mtype === 'video' && isUsingScreenStream) {
type = 'screen';
}
connection.emuc.addMediaToPresence(i, type, newssrcs[mtype], directions[mtype]);
}); });
if (i > 0) { if (i > 0) {
connection.emuc.sendPresence(); connection.emuc.sendPresence();
...@@ -422,7 +523,7 @@ $(document).bind('joined.muc', function (event, jid, info) { ...@@ -422,7 +523,7 @@ $(document).bind('joined.muc', function (event, jid, info) {
$(document).bind('entered.muc', function (event, jid, info, pres) { $(document).bind('entered.muc', function (event, jid, info, pres) {
console.log('entered', jid, info); console.log('entered', jid, info);
console.log(focus); console.log('is focus?' + focus ? 'true' : 'false');
var videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid); var videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
var container = addRemoteVideoContainer(videoSpanId); var container = addRemoteVideoContainer(videoSpanId);
...@@ -489,12 +590,21 @@ $(document).bind('left.muc', function (event, jid) { ...@@ -489,12 +590,21 @@ $(document).bind('left.muc', function (event, jid) {
}); });
$(document).bind('presence.muc', function (event, jid, info, pres) { $(document).bind('presence.muc', function (event, jid, info, pres) {
// Remove old ssrcs coming from the jid
Object.keys(ssrc2jid).forEach(function(ssrc){
if(ssrc2jid[ssrc] == jid){
delete ssrc2jid[ssrc];
}
});
$(pres).find('>media[xmlns="http://estos.de/ns/mjs"]>source').each(function (idx, ssrc) { $(pres).find('>media[xmlns="http://estos.de/ns/mjs"]>source').each(function (idx, ssrc) {
//console.log(jid, 'assoc ssrc', ssrc.getAttribute('type'), ssrc.getAttribute('ssrc')); //console.log(jid, 'assoc ssrc', ssrc.getAttribute('type'), ssrc.getAttribute('ssrc'));
ssrc2jid[ssrc.getAttribute('ssrc')] = jid; ssrc2jid[ssrc.getAttribute('ssrc')] = jid;
var type = ssrc.getAttribute('type');
// might need to update the direction if participant just went from sendrecv to recvonly // might need to update the direction if participant just went from sendrecv to recvonly
if (ssrc.getAttribute('type') === 'video') { if (type === 'video' || type === 'screen') {
var el = $('#participant_' + Strophe.getResourceFromJid(jid) + '>video'); var el = $('#participant_' + Strophe.getResourceFromJid(jid) + '>video');
switch(ssrc.getAttribute('direction')) { switch(ssrc.getAttribute('direction')) {
case 'sendrecv': case 'sendrecv':
...@@ -502,8 +612,18 @@ $(document).bind('presence.muc', function (event, jid, info, pres) { ...@@ -502,8 +612,18 @@ $(document).bind('presence.muc', function (event, jid, info, pres) {
break; break;
case 'recvonly': case 'recvonly':
el.hide(); el.hide();
// FIXME: Check if we have to change large video
//checkChangeLargeVideo(el);
break; break;
} }
// Camera video or shared screen ?
if (type === 'screen') {
// Shared screen
//console.info("Have screen ssrc from "+jid, ssrc);
} else {
// Camera video
//console.info("Have camera ssrc from "+jid, ssrc);
}
} }
}); });
...@@ -687,23 +807,27 @@ function isPresentationVisible() { ...@@ -687,23 +807,27 @@ function isPresentationVisible() {
/** /**
* Updates the large video with the given new video source. * Updates the large video with the given new video source.
*/ */
function updateLargeVideo(newSrc, localVideo, vol) { function updateLargeVideo(newSrc, vol) {
console.log('hover in', newSrc); console.log('hover in', newSrc);
setPresentationVisible(false); setPresentationVisible(false);
if ($('#largeVideo').attr('src') !== newSrc) { if ($('#largeVideo').attr('src') !== newSrc) {
document.getElementById('largeVideo').volume = vol; // FIXME: is it still required ? audio is separated
//document.getElementById('largeVideo').volume = vol;
$('#largeVideo').fadeOut(300, function () { $('#largeVideo').fadeOut(300, function () {
$(this).attr('src', newSrc); $(this).attr('src', newSrc);
// Screen stream is already rotated
var flipX = (newSrc === localVideoSrc) && flipXLocalVideo;
var videoTransform = document.getElementById('largeVideo').style.webkitTransform; var videoTransform = document.getElementById('largeVideo').style.webkitTransform;
if (localVideo && videoTransform !== 'scaleX(-1)') { if (flipX && videoTransform !== 'scaleX(-1)') {
document.getElementById('largeVideo').style.webkitTransform = "scaleX(-1)"; document.getElementById('largeVideo').style.webkitTransform = "scaleX(-1)";
} }
else if (!localVideo && videoTransform === 'scaleX(-1)') { else if (!flipX && videoTransform === 'scaleX(-1)') {
document.getElementById('largeVideo').style.webkitTransform = "none"; document.getElementById('largeVideo').style.webkitTransform = "none";
} }
...@@ -712,27 +836,34 @@ function updateLargeVideo(newSrc, localVideo, vol) { ...@@ -712,27 +836,34 @@ function updateLargeVideo(newSrc, localVideo, vol) {
} }
} }
function getConferenceHandler() {
return focus ? focus : activecall;
}
function toggleVideo() { function toggleVideo() {
if (!(connection && connection.jingle.localStream)) return; if (!(connection && connection.jingle.localVideo)) return;
var ismuted = false;
for (var idx = 0; idx < connection.jingle.localStream.getVideoTracks().length; idx++) { var sess = getConferenceHandler();
ismuted = !connection.jingle.localStream.getVideoTracks()[idx].enabled; if (sess) {
sess.toggleVideoMute(
function(isMuted){
if(isMuted) {
$('#video').removeClass("fa fa-video-camera fa-lg");
$('#video').addClass("fa fa-video-camera no-fa-video-camera fa-lg");
} else {
$('#video').removeClass("fa fa-video-camera no-fa-video-camera fa-lg");
$('#video').addClass("fa fa-video-camera fa-lg");
} }
for (var idx = 0; idx < connection.jingle.localStream.getVideoTracks().length; idx++) {
connection.jingle.localStream.getVideoTracks()[idx].enabled = !connection.jingle.localStream.getVideoTracks()[idx].enabled;
} }
var sess = focus || activecall; );
if (!sess) {
return;
} }
sess.pendingop = ismuted ? 'unmute' : 'mute';
sess.modifySources();
} }
function toggleAudio() { function toggleAudio() {
if (!(connection && connection.jingle.localStream)) return; if (!(connection && connection.jingle.localAudio)) return;
for (var idx = 0; idx < connection.jingle.localStream.getAudioTracks().length; idx++) { var localAudio = connection.jingle.localAudio;
connection.jingle.localStream.getAudioTracks()[idx].enabled = !connection.jingle.localStream.getAudioTracks()[idx].enabled; for (var idx = 0; idx < localAudio.getAudioTracks().length; idx++) {
localAudio.getAudioTracks()[idx].enabled = !localAudio.getAudioTracks()[idx].enabled;
} }
} }
...@@ -1132,6 +1263,8 @@ function showToolbar() { ...@@ -1132,6 +1263,8 @@ function showToolbar() {
// TODO: Enable settings functionality. Need to uncomment the settings button in index.html. // TODO: Enable settings functionality. Need to uncomment the settings button in index.html.
// $('#settingsButton').css({visibility:"visible"}); // $('#settingsButton').css({visibility:"visible"});
} }
// Set desktop sharing method
setDesktopSharing(config.desktopSharing);
} }
/* /*
...@@ -1170,6 +1303,10 @@ function showFocusIndicator() { ...@@ -1170,6 +1303,10 @@ function showFocusIndicator() {
var session = connection.jingle.sessions[Object.keys(connection.jingle.sessions)[0]]; var session = connection.jingle.sessions[Object.keys(connection.jingle.sessions)[0]];
var focusId = 'participant_' + Strophe.getResourceFromJid(session.peerjid); var focusId = 'participant_' + Strophe.getResourceFromJid(session.peerjid);
var focusContainer = document.getElementById(focusId); var focusContainer = document.getElementById(focusId);
if(!focusContainer) {
console.error("No focus container!");
return;
}
var indicatorSpan = $('#' + focusId + ' .focusindicator'); var indicatorSpan = $('#' + focusId + ' .focusindicator');
if (!indicatorSpan || indicatorSpan.length === 0) { if (!indicatorSpan || indicatorSpan.length === 0) {
......
...@@ -10,6 +10,10 @@ var config = { ...@@ -10,6 +10,10 @@ var config = {
useNicks: false, useNicks: false,
bosh: 'https://btg199251:7443/http-bind/', // FIXME: use xep-0156 for that bosh: 'https://btg199251:7443/http-bind/', // FIXME: use xep-0156 for that
desktopSharing: 'webrtc', // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable.
chromeExtensionId: 'diibjkoicjeejcmhdnailmkgecihlobk', // Id of desktop streamer Chrome extension
minChromeExtVersion: '0.0.8', // Required version of Chrome extension
getroomnode: function (path) getroomnode: function (path)
{ {
console.log('getroomnode', path); console.log('getroomnode', path);
......
...@@ -49,7 +49,7 @@ html, body{ ...@@ -49,7 +49,7 @@ html, body{
font-size: 10pt; font-size: 10pt;
} }
#localVideo { .flipVideoX {
-moz-transform: scaleX(-1); -moz-transform: scaleX(-1);
-webkit-transform: scaleX(-1); -webkit-transform: scaleX(-1);
-o-transform: scaleX(-1); -o-transform: scaleX(-1);
......
/**
* Indicates that desktop stream is currently in use(for toggle purpose).
* @type {boolean}
*/
var isUsingScreenStream = false;
/**
* Indicates that switch stream operation is in progress and prevent from triggering new events.
* @type {boolean}
*/
var switchInProgress = false;
/**
* Method used to get screen sharing stream.
*
* @type {function(stream_callback, failure_callback}
*/
var obtainDesktopStream = null;
/**
* @returns {boolean} <tt>true</tt> if desktop sharing feature is available and enabled.
*/
function isDesktopSharingEnabled() {
if(obtainDesktopStream === obtainScreenFromExtension) {
// Parse chrome version
var userAgent = navigator.userAgent.toLowerCase();
// We can assume that user agent is chrome, because it's enforced when 'ext' streaming method is set
var ver = parseInt(userAgent.match(/chrome\/(\d+)\./)[1], 10);
console.log("Chrome version" + userAgent, ver);
return ver >= 35;
} else {
return obtainDesktopStream === obtainWebRTCScreen;
}
}
/**
* Call this method to toggle desktop sharing feature.
* @param method pass "ext" to use chrome extension for desktop capture(chrome extension required),
* pass "webrtc" to use WebRTC "screen" desktop source('chrome://flags/#enable-usermedia-screen-capture'
* must be enabled), pass any other string or nothing in order to disable this feature completely.
*/
function setDesktopSharing(method) {
if(method == "ext") {
if(RTC.browser === 'chrome') {
obtainDesktopStream = obtainScreenFromExtension;
console.info("Using Chrome extension for desktop sharing");
} else {
console.error("Chrome is required to use extension method");
obtainDesktopStream = null;
}
} else if(method == "webrtc") {
obtainDesktopStream = obtainWebRTCScreen;
console.info("Using WebRTC for desktop sharing");
} else {
obtainDesktopStream = null;
console.info("Desktop sharing disabled");
}
showDesktopSharingButton();
}
function showDesktopSharingButton() {
if(isDesktopSharingEnabled()) {
$('#desktopsharing').css( {display:"inline"} );
} else {
$('#desktopsharing').css( {display:"none"} );
}
}
/*
* Toggles screen sharing.
*/
function toggleScreenSharing() {
if (switchInProgress || !obtainDesktopStream) {
console.warn("Switch in progress or no method defined");
return;
}
switchInProgress = true;
// Only the focus is able to set a shared key.
if(!isUsingScreenStream)
{
obtainDesktopStream(
function(stream) {
// We now use screen stream
isUsingScreenStream = true;
// Hook 'ended' event to restore camera when screen stream stops
stream.addEventListener('ended',
function(e) {
if(!switchInProgress && isUsingScreenStream) {
toggleScreenSharing();
}
}
);
newStreamCreated(stream);
},
getSwitchStreamFailed );
} else {
// Disable screen stream
getUserMediaWithConstraints(
['video'],
function(stream) {
// We are now using camera stream
isUsingScreenStream = false;
newStreamCreated(stream);
},
getSwitchStreamFailed, config.resolution || '360'
);
}
}
function getSwitchStreamFailed(error) {
console.error("Failed to obtain the stream to switch to", error);
switchInProgress = false;
}
function newStreamCreated(stream) {
var oldStream = connection.jingle.localVideo;
change_local_video(stream, !isUsingScreenStream);
var conferenceHandler = getConferenceHandler();
if(conferenceHandler) {
// FIXME: will block switchInProgress on true value in case of exception
conferenceHandler.switchStreams(stream, oldStream, streamSwitchDone);
} else {
// We are done immediately
console.error("No conference handler");
streamSwitchDone();
}
}
function streamSwitchDone() {
//window.setTimeout(
// function() {
switchInProgress = false;
// }, 100
//);
}
/**
* Method obtains desktop stream from WebRTC 'screen' source.
* Flag 'chrome://flags/#enable-usermedia-screen-capture' must be enabled.
*/
function obtainWebRTCScreen(streamCallback, failCallback) {
getUserMediaWithConstraints(
['screen'],
streamCallback,
failCallback
);
}
/**
* Asks Chrome extension to call chooseDesktopMedia and gets chrome 'desktop' stream for returned stream token.
*/
function obtainScreenFromExtension(streamCallback, failCallback) {
checkExtInstalled(
function(isInstalled) {
if(isInstalled) {
doGetStreamFromExtension(streamCallback, failCallback);
} else {
chrome.webstore.install(
"https://chrome.google.com/webstore/detail/" + config.chromeExtensionId,
function(arg) {
console.log("Extension installed successfully", arg);
// We need to reload the page in order to get the access to chrome.runtime
window.location.reload(false);
},
function(arg) {
console.log("Failed to install the extension", arg);
failCallback(arg);
}
);
}
}
);
}
function checkExtInstalled(isInstalledCallback) {
if(!chrome.runtime) {
// No API, so no extension for sure
isInstalledCallback(false);
return false;
}
chrome.runtime.sendMessage(
config.chromeExtensionId,
{ getVersion: true },
function(response){
if(!response || !response.version) {
// Communication failure - assume that no endpoint exists
console.warn("Extension not installed?: "+chrome.runtime.lastError);
isInstalledCallback(false);
} else {
// Check installed extension version
var extVersion = response.version;
console.log('Extension version is: '+extVersion);
var updateRequired = extVersion < config.minChromeExtVersion;
if(updateRequired) {
alert(
'Jitsi Desktop Streamer requires update. ' +
'Changes will take effect after next Chrome restart.' );
}
isInstalledCallback(!updateRequired);
}
}
);
}
function doGetStreamFromExtension(streamCallback, failCallback) {
// Sends 'getStream' msg to the extension. Extension id must be defined in the config.
chrome.runtime.sendMessage(
config.chromeExtensionId,
{ getStream: true},
function(response) {
if(!response) {
failCallback(chrome.runtime.lastError);
return;
}
console.log("Response from extension: "+response);
if(response.streamId) {
getUserMediaWithConstraints(
['desktop'],
function(stream) {
streamCallback(stream);
},
failCallback,
null, null, null,
response.streamId);
} else {
failCallback("Extension failed to get the stream");
}
}
);
}
...@@ -6,12 +6,16 @@ ...@@ -6,12 +6,16 @@
<script src="libs/strophe/strophe.jingle.bundle.js?v=8"></script> <script src="libs/strophe/strophe.jingle.bundle.js?v=8"></script>
<script src="libs/strophe/strophe.jingle.js?v=1"></script> <script src="libs/strophe/strophe.jingle.js?v=1"></script>
<script src="libs/strophe/strophe.jingle.sdp.js?v=1"></script> <script src="libs/strophe/strophe.jingle.sdp.js?v=1"></script>
<script src="libs/strophe/strophe.jingle.sdp.util.js?v=1"></script>
<script src="libs/strophe/strophe.jingle.sessionbase.js?v=1"></script>
<script src="libs/strophe/strophe.jingle.session.js?v=1"></script> <script src="libs/strophe/strophe.jingle.session.js?v=1"></script>
<script src="libs/colibri/colibri.focus.js?v=8"></script><!-- colibri focus implementation --> <script src="libs/colibri/colibri.focus.js?v=8"></script><!-- colibri focus implementation -->
<script src="libs/colibri/colibri.session.js?v=1"></script> <script src="libs/colibri/colibri.session.js?v=1"></script>
<script src="//code.jquery.com/ui/1.10.4/jquery-ui.js"></script> <script src="//code.jquery.com/ui/1.10.4/jquery-ui.js"></script>
<script src="config.js"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
<script src="muc.js?v=9"></script><!-- simple MUC library --> <script src="muc.js?v=9"></script><!-- simple MUC library -->
<script src="estos_log.js?v=2"></script><!-- simple stanza logger --> <script src="estos_log.js?v=2"></script><!-- simple stanza logger -->
<script src="desktopsharing.js?v=1"></script><!-- desktop sharing -->
<script src="app.js?v=23"></script><!-- application logic --> <script src="app.js?v=23"></script><!-- application logic -->
<script src="chat.js?v=3"></script><!-- chat logic --> <script src="chat.js?v=3"></script><!-- chat logic -->
<script src="util.js?v=2"></script><!-- utility functions --> <script src="util.js?v=2"></script><!-- utility functions -->
...@@ -22,9 +26,12 @@ ...@@ -22,9 +26,12 @@
<link rel="stylesheet" type="text/css" media="screen" href="css/main.css?v=19"/> <link rel="stylesheet" type="text/css" media="screen" href="css/main.css?v=19"/>
<link rel="stylesheet" href="css/jquery-impromptu.css?v=4"> <link rel="stylesheet" href="css/jquery-impromptu.css?v=4">
<link rel="stylesheet" href="css/modaldialog.css?v=3"> <link rel="stylesheet" href="css/modaldialog.css?v=3">
<!--
Link used for inline installation of chrome desktop streaming extension,
must contain the same extension id as defined in config.js -->
<link rel="chrome-webstore-item" href="https://chrome.google.com/webstore/detail/diibjkoicjeejcmhdnailmkgecihlobk">
<script src="libs/jquery-impromptu.js"></script> <script src="libs/jquery-impromptu.js"></script>
<script src="libs/jquery.autosize.js"></script> <script src="libs/jquery.autosize.js"></script>
<script src="config.js"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
<script src="libs/prezi_player.js?v=2"></script> <script src="libs/prezi_player.js?v=2"></script>
</head> </head>
<body> <body>
...@@ -35,7 +42,7 @@ ...@@ -35,7 +42,7 @@
<a class="button" onclick='buttonClick("#mute", "fa fa-microphone fa-lg fa fa-microphone-slash fa-lg");toggleAudio();'> <a class="button" onclick='buttonClick("#mute", "fa fa-microphone fa-lg fa fa-microphone-slash fa-lg");toggleAudio();'>
<i id="mute" title="Mute / unmute" class="fa fa-microphone fa-lg"></i></a> <i id="mute" title="Mute / unmute" class="fa fa-microphone fa-lg"></i></a>
<div class="header_button_separator"></div> <div class="header_button_separator"></div>
<a class="button" onclick='buttonClick("#video", "fa fa-video-camera fa-lg fa fa-video-camera no-fa-video-camera fa-lg");toggleVideo();'> <a class="button" onclick='buttonClick("#video");toggleVideo();'>
<i id="video" title="Start / stop camera" class="fa fa-video-camera fa-lg"></i></a> <i id="video" title="Start / stop camera" class="fa fa-video-camera fa-lg"></i></a>
<div class="header_button_separator"></div> <div class="header_button_separator"></div>
<a class="button" onclick="openLockDialog();"><i id="lockIcon" title="Lock/unlock room" class="fa fa-unlock fa-lg"></i></a> <a class="button" onclick="openLockDialog();"><i id="lockIcon" title="Lock/unlock room" class="fa fa-unlock fa-lg"></i></a>
...@@ -53,6 +60,10 @@ ...@@ -53,6 +60,10 @@
<a class="button" onclick='Etherpad.toggleEtherpad(0);'><i title="Open shared document" class="fa fa-file-text fa-lg"></i></a> <a class="button" onclick='Etherpad.toggleEtherpad(0);'><i title="Open shared document" class="fa fa-file-text fa-lg"></i></a>
</span> </span>
<div class="header_button_separator"></div> <div class="header_button_separator"></div>
<span id="desktopsharing" style="display: none">
<a class="button" onclick="toggleScreenSharing();"><i title="Share screen" class="fa fa-desktop fa-lg"></i></a>
<div class="header_button_separator"></div>
</span>
<a class="button" onclick='toggleFullScreen();'><i title="Enter / Exit Full Screen" class="fa fa-arrows-alt fa-lg"></i></a> <a class="button" onclick='toggleFullScreen();'><i title="Enter / Exit Full Screen" class="fa fa-arrows-alt fa-lg"></i></a>
</span> </span>
</div> </div>
...@@ -78,7 +89,8 @@ ...@@ -78,7 +89,8 @@
<div id="remoteVideos"> <div id="remoteVideos">
<span id="localVideoContainer" class="videocontainer"> <span id="localVideoContainer" class="videocontainer">
<span id="localNick"></span> <span id="localNick"></span>
<video id="localVideo" autoplay oncontextmenu="return false;" muted></video> <!--<video id="localVideo" autoplay oncontextmenu="return false;" muted></video> - is now per stream generated -->
<audio id="localAudio" autoplay oncontextmenu="return false;" muted></audio>
<span class="focusindicator"></span> <span class="focusindicator"></span>
</span> </span>
<audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio> <audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
...@@ -99,6 +111,7 @@ ...@@ -99,6 +111,7 @@
<textarea id="usermsg" placeholder='Enter text...' autofocus></textarea> <textarea id="usermsg" placeholder='Enter text...' autofocus></textarea>
</div> </div>
<a id="downloadlog" onclick='dump(event.target);'><i title="Download support information" class="fa fa-cloud-download"></i></a> <a id="downloadlog" onclick='dump(event.target);'><i title="Download support information" class="fa fa-cloud-download"></i></a>
<!-- Google Analytics -->
<script> <script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
...@@ -106,5 +119,6 @@ ...@@ -106,5 +119,6 @@
ga('create', 'UA-319188-14', 'jit.si'); ga('create', 'UA-319188-14', 'jit.si');
ga('send', 'pageview'); ga('send', 'pageview');
</script> </script>
<!-- End Google Analytics -->
</body> </body>
</html> </html>
...@@ -34,30 +34,24 @@ ...@@ -34,30 +34,24 @@
THE SOFTWARE. THE SOFTWARE.
*/ */
/* jshint -W117 */ /* jshint -W117 */
ColibriFocus.prototype = Object.create(SessionBase.prototype);
function ColibriFocus(connection, bridgejid) { function ColibriFocus(connection, bridgejid) {
this.connection = connection;
SessionBase.call(this, connection, Math.random().toString(36).substr(2, 12));
this.bridgejid = bridgejid; this.bridgejid = bridgejid;
this.peers = []; this.peers = [];
this.confid = null; this.confid = null;
this.peerconnection = null;
// media types of the conference // media types of the conference
this.media = ['audio', 'video']; this.media = ['audio', 'video'];
this.sid = Math.random().toString(36).substr(2, 12);
this.connection.jingle.sessions[this.sid] = this; this.connection.jingle.sessions[this.sid] = this;
this.mychannel = []; this.mychannel = [];
this.channels = []; this.channels = [];
this.remotessrc = {}; this.remotessrc = {};
// ssrc lines to be added on next update
this.addssrc = [];
// ssrc lines to be removed on next update
this.removessrc = [];
// pending mute/unmute video op that modify local description
this.pendingop = null;
// container for candidates from the focus // container for candidates from the focus
// gathered before confid is known // gathered before confid is known
this.drip_container = []; this.drip_container = [];
...@@ -81,8 +75,12 @@ ColibriFocus.prototype.makeConference = function (peers) { ...@@ -81,8 +75,12 @@ ColibriFocus.prototype.makeConference = function (peers) {
self.channels.push([]); self.channels.push([]);
}); });
this.peerconnection = new TraceablePeerConnection(this.connection.jingle.ice_config, this.connection.jingle.pc_constraints); if(connection.jingle.localAudio) {
this.peerconnection.addStream(this.connection.jingle.localStream); this.peerconnection.addStream(connection.jingle.localAudio);
}
if(connection.jingle.localVideo) {
this.peerconnection.addStream(connection.jingle.localVideo);
}
this.peerconnection.oniceconnectionstatechange = function (event) { this.peerconnection.oniceconnectionstatechange = function (event) {
console.warn('ice connection state changed to', self.peerconnection.iceConnectionState); console.warn('ice connection state changed to', self.peerconnection.iceConnectionState);
/* /*
...@@ -102,14 +100,10 @@ ColibriFocus.prototype.makeConference = function (peers) { ...@@ -102,14 +100,10 @@ ColibriFocus.prototype.makeConference = function (peers) {
*/ */
}; };
this.peerconnection.onaddstream = function (event) { this.peerconnection.onaddstream = function (event) {
self.remoteStream = event.stream;
// search the jid associated with this stream // search the jid associated with this stream
Object.keys(self.remotessrc).forEach(function (jid) { Object.keys(self.remotessrc).forEach(function (jid) {
if (self.remotessrc[jid].join('\r\n').indexOf('mslabel:' + event.stream.id) != -1) { if (self.remotessrc[jid].join('\r\n').indexOf('mslabel:' + event.stream.id) != -1) {
event.peerjid = jid; event.peerjid = jid;
if (self.connection.jingle.jid2session[jid]) {
self.connection.jingle.jid2session[jid].remotestream = event.stream;
}
} }
}); });
$(document).trigger('remotestreamadded.jingle', [event, self.sid]); $(document).trigger('remotestreamadded.jingle', [event, self.sid]);
...@@ -151,12 +145,18 @@ ColibriFocus.prototype._makeConference = function () { ...@@ -151,12 +145,18 @@ ColibriFocus.prototype._makeConference = function () {
var elem = $iq({to: this.bridgejid, type: 'get'}); var elem = $iq({to: this.bridgejid, type: 'get'});
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'}); elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'});
var stream = this.connection.jingle.localStream;
this.media.forEach(function (name) { this.media.forEach(function (name) {
elem.c('content', {name: name}); elem.c('content', {name: name});
elem.c('channel', {initiator: 'true', expire: '15'}).up(); elem.c('channel', {
initiator: 'true',
expire: '15',
endpoint: 'fix_me_focus_endpoint'}).up();
for (var j = 0; j < self.peers.length; j++) { for (var j = 0; j < self.peers.length; j++) {
elem.c('channel', {initiator: 'true', expire:'15' }).up(); elem.c('channel', {
initiator: 'true',
expire: '15',
endpoint: self.peers[j].substr(1 + self.peers[j].lastIndexOf('/'))
}).up();
} }
elem.up(); // end of content elem.up(); // end of content
}); });
...@@ -308,7 +308,8 @@ ColibriFocus.prototype.createdConference = function (result) { ...@@ -308,7 +308,8 @@ ColibriFocus.prototype.createdConference = function (result) {
elem.c('channel', { elem.c('channel', {
initiator: 'true', initiator: 'true',
expire: '15', expire: '15',
id: self.mychannel[channel].attr('id') id: self.mychannel[channel].attr('id'),
endpoint: 'fix_me_focus_endpoint'
}); });
// FIXME: should reuse code from .toJingle // FIXME: should reuse code from .toJingle
...@@ -448,7 +449,8 @@ ColibriFocus.prototype.initiate = function (peer, isInitiator) { ...@@ -448,7 +449,8 @@ ColibriFocus.prototype.initiate = function (peer, isInitiator) {
this.connection); this.connection);
sess.initiate(peer); sess.initiate(peer);
sess.colibri = this; sess.colibri = this;
sess.localStream = this.connection.jingle.localStream; // We do not announce our audio per conference peer, so only video is set here
sess.localVideo = this.connection.jingle.localVideo;
sess.media_constraints = this.connection.jingle.media_constraints; sess.media_constraints = this.connection.jingle.media_constraints;
sess.pc_constraints = this.connection.jingle.pc_constraints; sess.pc_constraints = this.connection.jingle.pc_constraints;
sess.ice_config = this.connection.jingle.ice_config; sess.ice_config = this.connection.jingle.ice_config;
...@@ -497,7 +499,11 @@ ColibriFocus.prototype.addNewParticipant = function (peer) { ...@@ -497,7 +499,11 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
localSDP.media.forEach(function (media, channel) { localSDP.media.forEach(function (media, channel) {
var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media; var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
elem.c('content', {name: name}); elem.c('content', {name: name});
elem.c('channel', {initiator: 'true', expire:'15'}); elem.c('channel', {
initiator: 'true',
expire:'15',
endpoint: peer.substr(1 + peer.lastIndexOf('/'))
});
elem.up(); // end of channel elem.up(); // end of channel
elem.up(); // end of content elem.up(); // end of content
}); });
...@@ -524,7 +530,11 @@ ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) { ...@@ -524,7 +530,11 @@ ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid}); change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
for (channel = 0; channel < this.channels[participant].length; channel++) { for (channel = 0; channel < this.channels[participant].length; channel++) {
change.c('content', {name: channel === 0 ? 'audio' : 'video'}); change.c('content', {name: channel === 0 ? 'audio' : 'video'});
change.c('channel', {id: $(this.channels[participant][channel]).attr('id')}); change.c('channel', {
id: $(this.channels[participant][channel]).attr('id'),
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
expire: '15'
});
var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:'); var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:');
rtpmap.forEach(function (val) { rtpmap.forEach(function (val) {
...@@ -561,75 +571,92 @@ ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) { ...@@ -561,75 +571,92 @@ ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
// tell everyone about a new participants a=ssrc lines (isadd is true) // tell everyone about a new participants a=ssrc lines (isadd is true)
// or a leaving participants a=ssrc lines // or a leaving participants a=ssrc lines
// FIXME: should not take an SDP, but rather the a=ssrc lines and probably a=mid ColibriFocus.prototype.sendSSRCUpdate = function (sdpMediaSsrcs, fromJid, isadd) {
ColibriFocus.prototype.sendSSRCUpdate = function (sdp, jid, isadd) {
var self = this; var self = this;
this.peers.forEach(function (peerjid) { this.peers.forEach(function (peerjid) {
if (peerjid == jid) return; if (peerjid == fromJid) return;
console.log('tell', peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from', jid); console.log('tell', peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from', fromJid);
if (!self.remotessrc[peerjid]) { if (!self.remotessrc[peerjid]) {
// FIXME: this should only send to participants that are stable, i.e. who have sent a session-accept // FIXME: this should only send to participants that are stable, i.e. who have sent a session-accept
// possibly, this.remoteSSRC[session.peerjid] does not exist yet // possibly, this.remoteSSRC[session.peerjid] does not exist yet
console.warn('do we really want to bother', peerjid, 'with updates yet?'); console.warn('do we really want to bother', peerjid, 'with updates yet?');
} }
var channel;
var peersess = self.connection.jingle.jid2session[peerjid]; var peersess = self.connection.jingle.jid2session[peerjid];
var modify = $iq({to: peerjid, type: 'set'}) if(!peersess){
.c('jingle', { console.warn('no session with peer: '+peerjid+' yet...');
xmlns: 'urn:xmpp:jingle:1', return;
action: isadd ? 'addsource' : 'removesource',
initiator: peersess.initiator,
sid: peersess.sid
}
);
// FIXME: only announce video ssrcs since we mix audio and dont need
// the audio ssrcs therefore
var modified = false;
for (channel = 0; channel < sdp.media.length; channel++) {
modified = true;
tmp = SDPUtil.find_lines(sdp.media[channel], 'a=ssrc:');
modify.c('content', {name: SDPUtil.parse_mid(SDPUtil.find_line(sdp.media[channel], 'a=mid:'))});
modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
// FIXME: not completly sure this operates on blocks and / or handles different ssrcs correctly
tmp.forEach(function (line) {
var idx = line.indexOf(' ');
var linessrc = line.substr(0, idx).substr(7);
modify.attrs({ssrc: linessrc});
var kv = line.substr(idx + 1);
modify.c('parameter');
if (kv.indexOf(':') == -1) {
modify.attrs({ name: kv });
} else {
modify.attrs({ name: kv.split(':', 2)[0] });
modify.attrs({ value: kv.split(':', 2)[1] });
} }
modify.up();
self.sendSSRCUpdateIq(sdpMediaSsrcs, peersess.sid, peersess.initiator, peerjid, isadd);
}); });
modify.up(); // end of source };
modify.up(); // end of content
/**
* Overrides SessionBase.addSource.
*
* @param elem proprietary 'add source' Jingle request(XML node).
* @param fromJid JID of the participant to whom new ssrcs belong.
*/
ColibriFocus.prototype.addSource = function (elem, fromJid) {
var self = this;
this.peerconnection.addSource(elem);
var peerSsrc = this.remotessrc[fromJid];
//console.log("On ADD", self.addssrc, peerSsrc);
this.peerconnection.addssrc.forEach(function(val, idx){
if(!peerSsrc[idx]){
// add ssrc
peerSsrc[idx] = val;
} else {
if(peerSsrc[idx].indexOf(val) == -1){
peerSsrc[idx] = peerSsrc[idx]+val;
} }
if (modified) {
self.connection.sendIQ(modify,
function (res) {
console.warn('got modify result');
},
function (err) {
console.warn('got modify error');
} }
); });
} else {
console.log('modification not necessary'); var oldRemoteSdp = new SDP(this.peerconnection.remoteDescription.sdp);
this.modifySources(function(){
// Notify other participants about added ssrc
var remoteSDP = new SDP(self.peerconnection.remoteDescription.sdp);
var newSSRCs = oldRemoteSdp.getNewMedia(remoteSDP);
self.sendSSRCUpdate(newSSRCs, fromJid, true);
});
};
/**
* Overrides SessionBase.removeSource.
*
* @param elem proprietary 'remove source' Jingle request(XML node).
* @param fromJid JID of the participant to whom removed ssrcs belong.
*/
ColibriFocus.prototype.removeSource = function (elem, fromJid) {
var self = this;
this.peerconnection.removeSource(elem);
var peerSsrc = this.remotessrc[fromJid];
//console.log("On REMOVE", self.removessrc, peerSsrc);
this.peerconnection.removessrc.forEach(function(val, idx){
if(peerSsrc[idx]){
// Remove ssrc
peerSsrc[idx] = peerSsrc[idx].replace(val, '');
} }
}); });
var oldSDP = new SDP(self.peerconnection.remoteDescription.sdp);
this.modifySources(function(){
// Notify other participants about removed ssrc
var remoteSDP = new SDP(self.peerconnection.remoteDescription.sdp);
var removedSSRCs = remoteSDP.getNewMedia(oldSDP);
self.sendSSRCUpdate(removedSSRCs, fromJid, false);
});
}; };
ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype) { ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype) {
var participant = this.peers.indexOf(session.peerjid); var participant = this.peers.indexOf(session.peerjid);
console.log('Colibri.setRemoteDescription from', session.peerjid, participant); console.log('Colibri.setRemoteDescription from', session.peerjid, participant);
var self = this;
var remoteSDP = new SDP(''); var remoteSDP = new SDP('');
var tmp;
var channel; var channel;
remoteSDP.fromJingle(elem); remoteSDP.fromJingle(elem);
...@@ -637,7 +664,7 @@ ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype) ...@@ -637,7 +664,7 @@ ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype)
this.updateChannel(remoteSDP, participant); this.updateChannel(remoteSDP, participant);
// ACT 2: tell anyone else about the new SSRCs // ACT 2: tell anyone else about the new SSRCs
this.sendSSRCUpdate(remoteSDP, session.peerjid, true); this.sendSSRCUpdate(remoteSDP.getMediaSsrcMap(), session.peerjid, true);
// ACT 3: note the SSRCs // ACT 3: note the SSRCs
this.remotessrc[session.peerjid] = []; this.remotessrc[session.peerjid] = [];
...@@ -651,9 +678,11 @@ ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype) ...@@ -651,9 +678,11 @@ ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype)
// ACT 4: add new a=ssrc lines to local remotedescription // ACT 4: add new a=ssrc lines to local remotedescription
for (channel = 0; channel < this.channels[participant].length; channel++) { for (channel = 0; channel < this.channels[participant].length; channel++) {
//if (channel == 0) continue; FIXME: does not work as intended //if (channel == 0) continue; FIXME: does not work as intended
if (!this.addssrc[channel]) this.addssrc[channel] = '';
if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length) { if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length) {
this.addssrc[channel] += SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n'; this.peerconnection.enqueueAddSsrc(
channel,
SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n'
);
} }
} }
this.modifySources(); this.modifySources();
...@@ -671,7 +700,11 @@ ColibriFocus.prototype.addIceCandidate = function (session, elem) { ...@@ -671,7 +700,11 @@ ColibriFocus.prototype.addIceCandidate = function (session, elem) {
var channel = name == 'audio' ? 0 : 1; // FIXME: search mlineindex in localdesc var channel = name == 'audio' ? 0 : 1; // FIXME: search mlineindex in localdesc
change.c('content', {name: name}); change.c('content', {name: name});
change.c('channel', {id: $(self.channels[participant][channel]).attr('id')}); change.c('channel', {
id: $(self.channels[participant][channel]).attr('id'),
endpoint: $(self.channels[participant][channel]).attr('endpoint'),
expire: '15'
});
$(this).find('>transport').each(function () { $(this).find('>transport').each(function () {
change.c('transport', { change.c('transport', {
ufrag: $(this).attr('ufrag'), ufrag: $(this).attr('ufrag'),
...@@ -700,7 +733,7 @@ ColibriFocus.prototype.addIceCandidate = function (session, elem) { ...@@ -700,7 +733,7 @@ ColibriFocus.prototype.addIceCandidate = function (session, elem) {
console.log('got result'); console.log('got result');
}, },
function (err) { function (err) {
console.warn('got error'); console.error('got error', err);
} }
); );
}; };
...@@ -735,7 +768,11 @@ ColibriFocus.prototype.sendIceCandidates = function (candidates) { ...@@ -735,7 +768,11 @@ ColibriFocus.prototype.sendIceCandidates = function (candidates) {
var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; }); var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
if (cands.length > 0) { if (cands.length > 0) {
mycands.c('content', {name: cands[0].sdpMid }); mycands.c('content', {name: cands[0].sdpMid });
mycands.c('channel', {id: $(this.mychannel[cands[0].sdpMLineIndex]).attr('id')}); mycands.c('channel', {
id: $(this.mychannel[cands[0].sdpMLineIndex]).attr('id'),
endpoint: $(this.mychannel[cands[0].sdpMLineIndex]).attr('endpoint'),
expire: '15'
});
mycands.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'}); mycands.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
for (var i = 0; i < cands.length; i++) { for (var i = 0; i < cands.length; i++) {
mycands.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up(); mycands.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
...@@ -751,7 +788,7 @@ ColibriFocus.prototype.sendIceCandidates = function (candidates) { ...@@ -751,7 +788,7 @@ ColibriFocus.prototype.sendIceCandidates = function (candidates) {
console.log('got result'); console.log('got result');
}, },
function (err) { function (err) {
console.warn('got error'); console.error('got error', err);
} }
); );
}; };
...@@ -764,8 +801,7 @@ ColibriFocus.prototype.terminate = function (session, reason) { ...@@ -764,8 +801,7 @@ ColibriFocus.prototype.terminate = function (session, reason) {
} }
var ssrcs = this.remotessrc[session.peerjid]; var ssrcs = this.remotessrc[session.peerjid];
for (var i = 0; i < ssrcs.length; i++) { for (var i = 0; i < ssrcs.length; i++) {
if (!this.removessrc[i]) this.removessrc[i] = ''; this.peerconnection.enqueueRemoveSsrc(i, ssrcs[i]);
this.removessrc[i] += ssrcs[i];
} }
// remove from this.peers // remove from this.peers
this.peers.splice(participant, 1); this.peers.splice(participant, 1);
...@@ -774,7 +810,11 @@ ColibriFocus.prototype.terminate = function (session, reason) { ...@@ -774,7 +810,11 @@ ColibriFocus.prototype.terminate = function (session, reason) {
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid}); change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
for (var channel = 0; channel < this.channels[participant].length; channel++) { for (var channel = 0; channel < this.channels[participant].length; channel++) {
change.c('content', {name: channel === 0 ? 'audio' : 'video'}); change.c('content', {name: channel === 0 ? 'audio' : 'video'});
change.c('channel', {id: $(this.channels[participant][channel]).attr('id'), expire: '0'}); change.c('channel', {
id: $(this.channels[participant][channel]).attr('id'),
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
expire: '0'
});
change.up(); // end of channel change.up(); // end of channel
change.up(); // end of content change.up(); // end of content
} }
...@@ -783,7 +823,7 @@ ColibriFocus.prototype.terminate = function (session, reason) { ...@@ -783,7 +823,7 @@ ColibriFocus.prototype.terminate = function (session, reason) {
console.log('got result'); console.log('got result');
}, },
function (err) { function (err) {
console.log('got error'); console.error('got error', err);
} }
); );
// and remove from channels // and remove from channels
...@@ -796,138 +836,10 @@ ColibriFocus.prototype.terminate = function (session, reason) { ...@@ -796,138 +836,10 @@ ColibriFocus.prototype.terminate = function (session, reason) {
for (var j = 0; j < ssrcs.length; j++) { for (var j = 0; j < ssrcs.length; j++) {
sdp.media[j] = 'a=mid:' + contents[j] + '\r\n'; sdp.media[j] = 'a=mid:' + contents[j] + '\r\n';
sdp.media[j] += ssrcs[j]; sdp.media[j] += ssrcs[j];
this.removessrc[j] += ssrcs[j]; this.peerconnection.enqueueRemoveSsrc(j, ssrcs[j]);
} }
this.sendSSRCUpdate(sdp, session.peerjid, false); this.sendSSRCUpdate(sdp.getMediaSsrcMap(), session.peerjid, false);
delete this.remotessrc[session.peerjid]; delete this.remotessrc[session.peerjid];
this.modifySources(); this.modifySources();
}; };
ColibriFocus.prototype.modifySources = function () {
var self = this;
if (this.peerconnection.signalingState == 'closed') return;
if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null)) return;
// FIXME: this is a big hack
// https://code.google.com/p/webrtc/issues/detail?id=2688
if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')) {
console.warn('modifySources not yet', this.peerconnection.signalingState, this.peerconnection.iceConnectionState);
window.setTimeout(function () { self.modifySources(); }, 250);
this.wait = true;
return;
}
if (this.wait) {
window.setTimeout(function () { self.modifySources(); }, 2500);
this.wait = false;
return;
}
var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
// add sources
this.addssrc.forEach(function (lines, idx) {
sdp.media[idx] += lines;
});
this.addssrc = [];
// remove sources
this.removessrc.forEach(function (lines, idx) {
lines = lines.split('\r\n');
lines.pop(); // remove empty last element;
lines.forEach(function (line) {
sdp.media[idx] = sdp.media[idx].replace(line + '\r\n', '');
});
});
this.removessrc = [];
sdp.raw = sdp.session + sdp.media.join('');
this.peerconnection.setRemoteDescription(
new RTCSessionDescription({type: 'offer', sdp: sdp.raw }),
function () {
console.log('setModifiedRemoteDescription ok');
self.peerconnection.createAnswer(
function (modifiedAnswer) {
console.log('modifiedAnswer created');
// change video direction, see https://github.com/jitsi/jitmeet/issues/41
if (self.pendingop !== null) {
var sdp = new SDP(modifiedAnswer.sdp);
if (sdp.media.length > 1) {
switch(self.pendingop) {
case 'mute':
sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
break;
case 'unmute':
sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
break;
}
sdp.raw = sdp.session + sdp.media.join('');
modifiedAnswer.sdp = sdp.raw;
}
self.pendingop = null;
}
// FIXME: pushing down an answer while ice connection state
// is still checking is bad...
//console.log(self.peerconnection.iceConnectionState);
// trying to work around another chrome bug
//modifiedAnswer.sdp = modifiedAnswer.sdp.replace(/a=setup:active/g, 'a=setup:actpass');
self.peerconnection.setLocalDescription(modifiedAnswer,
function () {
console.log('setModifiedLocalDescription ok');
$(document).trigger('setLocalDescription.jingle', [self.sid]);
},
function (error) {
console.log('setModifiedLocalDescription failed', error);
}
);
},
function (error) {
console.log('createModifiedAnswer failed', error);
}
);
},
function (error) {
console.log('setModifiedRemoteDescription failed', error);
}
);
/*
* now that we have a passive focus, this way is bad again! :-)
this.peerconnection.createOffer(
function (modifiedOffer) {
console.log('created (un)modified offer');
self.peerconnection.setLocalDescription(modifiedOffer,
function () {
console.log('setModifiedLocalDescription ok');
self.peerconnection.setRemoteDescription(
new RTCSessionDescription({type: 'answer', sdp: sdp.raw }),
function () {
console.log('setModifiedRemoteDescription ok');
},
function (error) {
console.log('setModifiedRemoteDescription failed');
}
);
$(document).trigger('setLocalDescription.jingle', [self.sid]);
},
function (error) {
console.log('setModifiedLocalDescription failed');
}
);
},
function (error) {
console.log('creating (un)modified offerfailed');
}
);
*/
};
ColibriFocus.prototype.hardMuteVideo = function (muted) {
this.pendingop = muted ? 'mute' : 'unmute';
this.modifySources();
this.connection.jingle.localStream.getVideoTracks().forEach(function (track) {
track.enabled = !muted;
});
};
...@@ -61,6 +61,14 @@ ColibriSession.prototype.accept = function () { ...@@ -61,6 +61,14 @@ ColibriSession.prototype.accept = function () {
console.log('ColibriSession.accept'); console.log('ColibriSession.accept');
}; };
ColibriSession.prototype.addSource = function (elem, fromJid) {
this.colibri.addSource(elem, fromJid);
};
ColibriSession.prototype.removeSource = function (elem, fromJid) {
this.colibri.removeSource(elem, fromJid);
};
ColibriSession.prototype.terminate = function (reason) { ColibriSession.prototype.terminate = function (reason) {
this.colibri.terminate(this, reason); this.colibri.terminate(this, reason);
}; };
......
...@@ -7,6 +7,30 @@ function TraceablePeerConnection(ice_config, constraints) { ...@@ -7,6 +7,30 @@ function TraceablePeerConnection(ice_config, constraints) {
this.statsinterval = null; this.statsinterval = null;
this.maxstats = 300; // limit to 300 values, i.e. 5 minutes; set to 0 to disable this.maxstats = 300; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
/**
* Array of ssrcs that will be added on next modifySources call.
* @type {Array}
*/
this.addssrc = [];
/**
* Array of ssrcs that will be added on next modifySources call.
* @type {Array}
*/
this.removessrc = [];
/**
* Pending operation that will be done during modifySources call.
* Currently 'mute'/'unmute' operations are supported.
*
* @type {String}
*/
this.pendingop = null;
/**
* Flag indicates that peer connection stream have changed and modifySources should proceed.
* @type {boolean}
*/
this.switchstreams = false;
// override as desired // override as desired
this.trace = function(what, info) { this.trace = function(what, info) {
//console.warn('WTRACE', what, info); //console.warn('WTRACE', what, info);
...@@ -163,6 +187,196 @@ TraceablePeerConnection.prototype.setRemoteDescription = function (description, ...@@ -163,6 +187,196 @@ TraceablePeerConnection.prototype.setRemoteDescription = function (description,
*/ */
}; };
TraceablePeerConnection.prototype.hardMuteVideo = function (muted) {
this.pendingop = muted ? 'mute' : 'unmute';
};
TraceablePeerConnection.prototype.enqueueAddSsrc = function(channel, ssrcLines) {
if (!this.addssrc[channel]) {
this.addssrc[channel] = '';
}
this.addssrc[channel] += ssrcLines;
}
TraceablePeerConnection.prototype.addSource = function (elem) {
console.log('addssrc', new Date().getTime());
console.log('ice', this.iceConnectionState);
var sdp = new SDP(this.remoteDescription.sdp);
var mySdp = new SDP(this.peerconnection.localDescription.sdp);
var self = this;
$(elem).each(function (idx, content) {
var name = $(content).attr('name');
var lines = '';
tmp = $(content).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
tmp.each(function () {
var ssrc = $(this).attr('ssrc');
if(mySdp.containsSSRC(ssrc)){
/**
* This happens when multiple participants change their streams at the same time and
* ColibriFocus.modifySources have to wait for stable state. In the meantime multiple
* addssrc are scheduled for update IQ. See
*/
console.warn("Got add stream request for my own ssrc: "+ssrc);
return;
}
$(this).find('>parameter').each(function () {
lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
if ($(this).attr('value') && $(this).attr('value').length)
lines += ':' + $(this).attr('value');
lines += '\r\n';
});
});
sdp.media.forEach(function(media, idx) {
if (!SDPUtil.find_line(media, 'a=mid:' + name))
return;
sdp.media[idx] += lines;
self.enqueueAddSsrc(idx, lines);
});
sdp.raw = sdp.session + sdp.media.join('');
});
};
TraceablePeerConnection.prototype.enqueueRemoveSsrc = function(channel, ssrcLines) {
if (!this.removessrc[channel]){
this.removessrc[channel] = '';
}
this.removessrc[channel] += ssrcLines;
}
TraceablePeerConnection.prototype.removeSource = function (elem) {
console.log('removessrc', new Date().getTime());
console.log('ice', this.iceConnectionState);
var sdp = new SDP(this.remoteDescription.sdp);
var mySdp = new SDP(this.peerconnection.localDescription.sdp);
var self = this;
$(elem).each(function (idx, content) {
var name = $(content).attr('name');
var lines = '';
tmp = $(content).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
tmp.each(function () {
var ssrc = $(this).attr('ssrc');
// This should never happen, but can be useful for bug detection
if(mySdp.containsSSRC(ssrc)){
console.error("Got remove stream request for my own ssrc: "+ssrc);
return;
}
$(this).find('>parameter').each(function () {
lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
if ($(this).attr('value') && $(this).attr('value').length)
lines += ':' + $(this).attr('value');
lines += '\r\n';
});
});
sdp.media.forEach(function(media, idx) {
if (!SDPUtil.find_line(media, 'a=mid:' + name))
return;
sdp.media[idx] += lines;
self.enqueueRemoveSsrc(idx, lines);
});
sdp.raw = sdp.session + sdp.media.join('');
});
};
TraceablePeerConnection.prototype.modifySources = function(successCallback) {
var self = this;
if (this.signalingState == 'closed') return;
if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null || this.switchstreams)){
// There is nothing to do since scheduled job might have been executed by another succeeding call
if(successCallback){
successCallback();
}
return;
}
// FIXME: this is a big hack
// https://code.google.com/p/webrtc/issues/detail?id=2688
if (!(this.signalingState == 'stable' && this.iceConnectionState == 'connected')) {
console.warn('modifySources not yet', this.signalingState, this.iceConnectionState);
this.wait = true;
window.setTimeout(function() { self.modifySources(successCallback); }, 250);
return;
}
if (this.wait) {
window.setTimeout(function() { self.modifySources(successCallback); }, 2500);
this.wait = false;
return;
}
// Reset switch streams flag
this.switchstreams = false;
var sdp = new SDP(this.remoteDescription.sdp);
// add sources
this.addssrc.forEach(function(lines, idx) {
sdp.media[idx] += lines;
});
this.addssrc = [];
// remove sources
this.removessrc.forEach(function(lines, idx) {
lines = lines.split('\r\n');
lines.pop(); // remove empty last element;
lines.forEach(function(line) {
sdp.media[idx] = sdp.media[idx].replace(line + '\r\n', '');
});
});
this.removessrc = [];
sdp.raw = sdp.session + sdp.media.join('');
this.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: sdp.raw}),
function() {
self.createAnswer(
function(modifiedAnswer) {
// change video direction, see https://github.com/jitsi/jitmeet/issues/41
if (self.pendingop !== null) {
var sdp = new SDP(modifiedAnswer.sdp);
if (sdp.media.length > 1) {
switch(self.pendingop) {
case 'mute':
sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
break;
case 'unmute':
sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
break;
}
sdp.raw = sdp.session + sdp.media.join('');
modifiedAnswer.sdp = sdp.raw;
}
self.pendingop = null;
}
// FIXME: pushing down an answer while ice connection state
// is still checking is bad...
//console.log(self.peerconnection.iceConnectionState);
// trying to work around another chrome bug
//modifiedAnswer.sdp = modifiedAnswer.sdp.replace(/a=setup:active/g, 'a=setup:actpass');
self.setLocalDescription(modifiedAnswer,
function() {
//console.log('modified setLocalDescription ok');
if(successCallback){
successCallback();
}
},
function(error) {
console.error('modified setLocalDescription failed', error);
}
);
},
function(error) {
console.error('modified answer failed', error);
}
);
},
function(error) {
console.error('modify failed', error);
}
);
};
TraceablePeerConnection.prototype.close = function () { TraceablePeerConnection.prototype.close = function () {
this.trace('stop'); this.trace('stop');
if (this.statsinterval !== null) { if (this.statsinterval !== null) {
...@@ -286,7 +500,7 @@ function setupRTC() { ...@@ -286,7 +500,7 @@ function setupRTC() {
return RTC; return RTC;
} }
function getUserMediaWithConstraints(um, resolution, bandwidth, fps) { function getUserMediaWithConstraints(um, success_callback, failure_callback, resolution, bandwidth, fps, desktopStream) {
var constraints = {audio: false, video: false}; var constraints = {audio: false, video: false};
if (um.indexOf('video') >= 0) { if (um.indexOf('video') >= 0) {
...@@ -297,11 +511,25 @@ function getUserMediaWithConstraints(um, resolution, bandwidth, fps) { ...@@ -297,11 +511,25 @@ function getUserMediaWithConstraints(um, resolution, bandwidth, fps) {
} }
if (um.indexOf('screen') >= 0) { if (um.indexOf('screen') >= 0) {
constraints.video = { constraints.video = {
"mandatory": { mandatory: {
"chromeMediaSource": "screen" chromeMediaSource: "screen",
maxWidth: window.screen.width,
maxHeight: window.screen.height,
maxFrameRate: 3
} }
}; };
} }
if (um.indexOf('desktop') >= 0) {
constraints.video = {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: desktopStream,
maxWidth: window.screen.width,
maxHeight: window.screen.height,
maxFrameRate: 3
}
}
}
if (resolution && !constraints.video) { if (resolution && !constraints.video) {
constraints.video = {mandatory: {}};// same behaviour as true constraints.video = {mandatory: {}};// same behaviour as true
...@@ -368,14 +596,18 @@ function getUserMediaWithConstraints(um, resolution, bandwidth, fps) { ...@@ -368,14 +596,18 @@ function getUserMediaWithConstraints(um, resolution, bandwidth, fps) {
RTC.getUserMedia(constraints, RTC.getUserMedia(constraints,
function (stream) { function (stream) {
console.log('onUserMediaSuccess'); console.log('onUserMediaSuccess');
$(document).trigger('mediaready.jingle', [stream]); success_callback(stream);
}, },
function (error) { function (error) {
console.warn('Failed to get access to local media. Error ', error); console.warn('Failed to get access to local media. Error ', error);
$(document).trigger('mediafailure.jingle'); if(failure_callback) {
failure_callback(error);
}
}); });
} catch (e) { } catch (e) {
console.error('GUM failed: ', e); console.error('GUM failed: ', e);
$(document).trigger('mediafailure.jingle'); if(failure_callback) {
failure_callback(e);
}
} }
} }
\ No newline at end of file
...@@ -12,7 +12,8 @@ Strophe.addConnectionPlugin('jingle', { ...@@ -12,7 +12,8 @@ Strophe.addConnectionPlugin('jingle', {
} }
// MozDontOfferDataChannel: true when this is firefox // MozDontOfferDataChannel: true when this is firefox
}, },
localStream: null, localAudio: null,
localVideo: null,
init: function (conn) { init: function (conn) {
this.connection = conn; this.connection = conn;
...@@ -38,12 +39,13 @@ Strophe.addConnectionPlugin('jingle', { ...@@ -38,12 +39,13 @@ Strophe.addConnectionPlugin('jingle', {
onJingle: function (iq) { onJingle: function (iq) {
var sid = $(iq).find('jingle').attr('sid'); var sid = $(iq).find('jingle').attr('sid');
var action = $(iq).find('jingle').attr('action'); var action = $(iq).find('jingle').attr('action');
var fromJid = iq.getAttribute('from');
// send ack first // send ack first
var ack = $iq({type: 'result', var ack = $iq({type: 'result',
to: iq.getAttribute('from'), to: fromJid,
id: iq.getAttribute('id') id: iq.getAttribute('id')
}); });
console.log('on jingle ' + action); console.log('on jingle ' + action + ' from ' + fromJid, iq);
var sess = this.sessions[sid]; var sess = this.sessions[sid];
if ('session-initiate' != action) { if ('session-initiate' != action) {
if (sess === null) { if (sess === null) {
...@@ -56,8 +58,8 @@ Strophe.addConnectionPlugin('jingle', { ...@@ -56,8 +58,8 @@ Strophe.addConnectionPlugin('jingle', {
} }
// compare from to sess.peerjid (bare jid comparison for later compat with message-mode) // compare from to sess.peerjid (bare jid comparison for later compat with message-mode)
// local jid is not checked // local jid is not checked
if (Strophe.getBareJidFromJid(iq.getAttribute('from')) != Strophe.getBareJidFromJid(sess.peerjid)) { if (Strophe.getBareJidFromJid(fromJid) != Strophe.getBareJidFromJid(sess.peerjid)) {
console.warn('jid mismatch for session id', sid, iq.getAttribute('from'), sess.peerjid); console.warn('jid mismatch for session id', sid, fromJid, sess.peerjid);
ack.type = 'error'; ack.type = 'error';
ack.c('error', {type: 'cancel'}) ack.c('error', {type: 'cancel'})
.c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up() .c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
...@@ -82,14 +84,17 @@ Strophe.addConnectionPlugin('jingle', { ...@@ -82,14 +84,17 @@ Strophe.addConnectionPlugin('jingle', {
case 'session-initiate': case 'session-initiate':
sess = new JingleSession($(iq).attr('to'), $(iq).find('jingle').attr('sid'), this.connection); sess = new JingleSession($(iq).attr('to'), $(iq).find('jingle').attr('sid'), this.connection);
// configure session // configure session
if (this.localStream) { if (this.localAudio) {
sess.localStreams.push(this.localStream); sess.localStreams.push(this.localAudio);
}
if (this.localVideo) {
sess.localStreams.push(this.localVideo);
} }
sess.media_constraints = this.media_constraints; sess.media_constraints = this.media_constraints;
sess.pc_constraints = this.pc_constraints; sess.pc_constraints = this.pc_constraints;
sess.ice_config = this.ice_config; sess.ice_config = this.ice_config;
sess.initiate($(iq).attr('from'), false); sess.initiate(fromJid, false);
// FIXME: setRemoteDescription should only be done when this call is to be accepted // FIXME: setRemoteDescription should only be done when this call is to be accepted
sess.setRemoteDescription($(iq).find('>jingle'), 'offer'); sess.setRemoteDescription($(iq).find('>jingle'), 'offer');
...@@ -136,10 +141,10 @@ Strophe.addConnectionPlugin('jingle', { ...@@ -136,10 +141,10 @@ Strophe.addConnectionPlugin('jingle', {
} }
break; break;
case 'addsource': // FIXME: proprietary case 'addsource': // FIXME: proprietary
sess.addSource($(iq).find('>jingle>content')); sess.addSource($(iq).find('>jingle>content'), fromJid);
break; break;
case 'removesource': // FIXME: proprietary case 'removesource': // FIXME: proprietary
sess.removeSource($(iq).find('>jingle>content')); sess.removeSource($(iq).find('>jingle>content'), fromJid);
break; break;
default: default:
console.warn('jingle action not implemented', action); console.warn('jingle action not implemented', action);
...@@ -152,8 +157,11 @@ Strophe.addConnectionPlugin('jingle', { ...@@ -152,8 +157,11 @@ Strophe.addConnectionPlugin('jingle', {
Math.random().toString(36).substr(2, 12), // random string Math.random().toString(36).substr(2, 12), // random string
this.connection); this.connection);
// configure session // configure session
if (this.localStream) { if (this.localAudio) {
sess.localStreams.push(this.localStream); sess.localStreams.push(this.localAudio);
}
if (this.localVideo) {
sess.localStreams.push(this.localVideo);
} }
sess.media_constraints = this.media_constraints; sess.media_constraints = this.media_constraints;
sess.pc_constraints = this.pc_constraints; sess.pc_constraints = this.pc_constraints;
......
...@@ -11,7 +11,75 @@ function SDP(sdp) { ...@@ -11,7 +11,75 @@ function SDP(sdp) {
this.session = this.media.shift() + '\r\n'; this.session = this.media.shift() + '\r\n';
this.raw = this.session + this.media.join(''); this.raw = this.session + this.media.join('');
} }
/**
* Returns map of MediaChannel mapped per channel idx.
*/
SDP.prototype.getMediaSsrcMap = function() {
var self = this;
var media_ssrcs = {};
for (channelNum = 0; channelNum < self.media.length; channelNum++) {
modified = true;
tmp = SDPUtil.find_lines(self.media[channelNum], 'a=ssrc:');
var type = SDPUtil.parse_mid(SDPUtil.find_line(self.media[channelNum], 'a=mid:'));
var channel = new MediaChannel(channelNum, type);
media_ssrcs[channelNum] = channel;
tmp.forEach(function (line) {
var linessrc = line.substring(7).split(' ')[0];
// allocate new ChannelSsrc
if(!channel.ssrcs[linessrc]) {
channel.ssrcs[linessrc] = new ChannelSsrc(linessrc, type);
}
channel.ssrcs[linessrc].lines.push(line);
});
}
return media_ssrcs;
}
/**
* Returns <tt>true</tt> if this SDP contains given SSRC.
* @param ssrc the ssrc to check.
* @returns {boolean} <tt>true</tt> if this SDP contains given SSRC.
*/
SDP.prototype.containsSSRC = function(ssrc) {
var channels = this.getMediaSsrcMap();
var contains = false;
Object.keys(channels).forEach(function(chNumber){
var channel = channels[chNumber];
//console.log("Check", channel, ssrc);
if(Object.keys(channel.ssrcs).indexOf(ssrc) != -1){
contains = true;
}
});
return contains;
}
/**
* Returns map of MediaChannel that contains only media not contained in <tt>otherSdp</tt>. Mapped by channel idx.
* @param otherSdp the other SDP to check ssrc with.
*/
SDP.prototype.getNewMedia = function(otherSdp) {
var myMedia = this.getMediaSsrcMap();
var othersMedia = otherSdp.getMediaSsrcMap();
var newMedia = {};
Object.keys(othersMedia).forEach(function(channelNum) {
var myChannel = myMedia[channelNum];
var othersChannel = othersMedia[channelNum];
if(!myChannel && othersChannel) {
// Add whole channel
newMedia[channelNum] = othersChannel;
return;
}
// Look for new ssrcs accross the channel
Object.keys(othersChannel.ssrcs).forEach(function(ssrc) {
if(Object.keys(myChannel.ssrcs).indexOf(ssrc) === -1) {
// Allocate channel if we've found ssrc that doesn't exist in our channel
if(!newMedia[channelNum]){
newMedia[channelNum] = new MediaChannel(othersChannel.chNumber, othersChannel.mediaType);
}
newMedia[channelNum].ssrcs[ssrc] = othersChannel.ssrcs[ssrc];
}
})
});
return newMedia;
}
// remove iSAC and CN from SDP // remove iSAC and CN from SDP
SDP.prototype.mangle = function () { SDP.prototype.mangle = function () {
var i, j, mline, lines, rtpmap, newdesc; var i, j, mline, lines, rtpmap, newdesc;
...@@ -486,316 +554,3 @@ SDP.prototype.jingle2media = function (content) { ...@@ -486,316 +554,3 @@ SDP.prototype.jingle2media = function (content) {
} }
return media; return media;
}; };
\ No newline at end of file
SDPUtil = {
iceparams: function (mediadesc, sessiondesc) {
var data = null;
if (SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc) &&
SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc)) {
data = {
ufrag: SDPUtil.parse_iceufrag(SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc)),
pwd: SDPUtil.parse_icepwd(SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc))
};
}
return data;
},
parse_iceufrag: function (line) {
return line.substring(12);
},
build_iceufrag: function (frag) {
return 'a=ice-ufrag:' + frag;
},
parse_icepwd: function (line) {
return line.substring(10);
},
build_icepwd: function (pwd) {
return 'a=ice-pwd:' + pwd;
},
parse_mid: function (line) {
return line.substring(6);
},
parse_mline: function (line) {
var parts = line.substring(2).split(' '),
data = {};
data.media = parts.shift();
data.port = parts.shift();
data.proto = parts.shift();
if (parts[parts.length - 1] === '') { // trailing whitespace
parts.pop();
}
data.fmt = parts;
return data;
},
build_mline: function (mline) {
return 'm=' + mline.media + ' ' + mline.port + ' ' + mline.proto + ' ' + mline.fmt.join(' ');
},
parse_rtpmap: function (line) {
var parts = line.substring(9).split(' '),
data = {};
data.id = parts.shift();
parts = parts[0].split('/');
data.name = parts.shift();
data.clockrate = parts.shift();
data.channels = parts.length ? parts.shift() : '1';
return data;
},
build_rtpmap: function (el) {
var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
line += '/' + el.getAttribute('channels');
}
return line;
},
parse_crypto: function (line) {
var parts = line.substring(9).split(' '),
data = {};
data.tag = parts.shift();
data['crypto-suite'] = parts.shift();
data['key-params'] = parts.shift();
if (parts.length) {
data['session-params'] = parts.join(' ');
}
return data;
},
parse_fingerprint: function (line) { // RFC 4572
var parts = line.substring(14).split(' '),
data = {};
data.hash = parts.shift();
data.fingerprint = parts.shift();
// TODO assert that fingerprint satisfies 2UHEX *(":" 2UHEX) ?
return data;
},
parse_fmtp: function (line) {
var parts = line.split(' '),
i, key, value,
data = [];
parts.shift();
parts = parts.join(' ').split(';');
for (i = 0; i < parts.length; i++) {
key = parts[i].split('=')[0];
while (key.length && key[0] == ' ') {
key = key.substring(1);
}
value = parts[i].split('=')[1];
if (key && value) {
data.push({name: key, value: value});
} else if (key) {
// rfc 4733 (DTMF) style stuff
data.push({name: '', value: key});
}
}
return data;
},
parse_icecandidate: function (line) {
var candidate = {},
elems = line.split(' ');
candidate.foundation = elems[0].substring(12);
candidate.component = elems[1];
candidate.protocol = elems[2].toLowerCase();
candidate.priority = elems[3];
candidate.ip = elems[4];
candidate.port = elems[5];
// elems[6] => "typ"
candidate.type = elems[7];
candidate.generation = 0; // default value, may be overwritten below
for (var i = 8; i < elems.length; i += 2) {
switch (elems[i]) {
case 'raddr':
candidate['rel-addr'] = elems[i + 1];
break;
case 'rport':
candidate['rel-port'] = elems[i + 1];
break;
case 'generation':
candidate.generation = elems[i + 1];
break;
default: // TODO
console.log('parse_icecandidate not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
}
}
candidate.network = '1';
candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
return candidate;
},
build_icecandidate: function (cand) {
var line = ['a=candidate:' + cand.foundation, cand.component, cand.protocol, cand.priority, cand.ip, cand.port, 'typ', cand.type].join(' ');
line += ' ';
switch (cand.type) {
case 'srflx':
case 'prflx':
case 'relay':
if (cand.hasOwnAttribute('rel-addr') && cand.hasOwnAttribute('rel-port')) {
line += 'raddr';
line += ' ';
line += cand['rel-addr'];
line += ' ';
line += 'rport';
line += ' ';
line += cand['rel-port'];
line += ' ';
}
break;
}
line += 'generation';
line += ' ';
line += cand.hasOwnAttribute('generation') ? cand.generation : '0';
return line;
},
parse_ssrc: function (desc) {
// proprietary mapping of a=ssrc lines
// TODO: see "Jingle RTP Source Description" by Juberti and P. Thatcher on google docs
// and parse according to that
var lines = desc.split('\r\n'),
data = {};
for (var i = 0; i < lines.length; i++) {
if (lines[i].substring(0, 7) == 'a=ssrc:') {
var idx = lines[i].indexOf(' ');
data[lines[i].substr(idx + 1).split(':', 2)[0]] = lines[i].substr(idx + 1).split(':', 2)[1];
}
}
return data;
},
parse_rtcpfb: function (line) {
var parts = line.substr(10).split(' ');
var data = {};
data.pt = parts.shift();
data.type = parts.shift();
data.params = parts;
return data;
},
parse_extmap: function (line) {
var parts = line.substr(9).split(' ');
var data = {};
data.value = parts.shift();
if (data.value.indexOf('/') != -1) {
data.direction = data.value.substr(data.value.indexOf('/') + 1);
data.value = data.value.substr(0, data.value.indexOf('/'));
} else {
data.direction = 'both';
}
data.uri = parts.shift();
data.params = parts;
return data;
},
find_line: function (haystack, needle, sessionpart) {
var lines = haystack.split('\r\n');
for (var i = 0; i < lines.length; i++) {
if (lines[i].substring(0, needle.length) == needle) {
return lines[i];
}
}
if (!sessionpart) {
return false;
}
// search session part
lines = sessionpart.split('\r\n');
for (var j = 0; j < lines.length; j++) {
if (lines[j].substring(0, needle.length) == needle) {
return lines[j];
}
}
return false;
},
find_lines: function (haystack, needle, sessionpart) {
var lines = haystack.split('\r\n'),
needles = [];
for (var i = 0; i < lines.length; i++) {
if (lines[i].substring(0, needle.length) == needle)
needles.push(lines[i]);
}
if (needles.length || !sessionpart) {
return needles;
}
// search session part
lines = sessionpart.split('\r\n');
for (var j = 0; j < lines.length; j++) {
if (lines[j].substring(0, needle.length) == needle) {
needles.push(lines[j]);
}
}
return needles;
},
candidateToJingle: function (line) {
// a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host generation 0
// <candidate component=... foundation=... generation=... id=... ip=... network=... port=... priority=... protocol=... type=.../>
if (line.substring(0, 12) != 'a=candidate:') {
console.log('parseCandidate called with a line that is not a candidate line');
console.log(line);
return null;
}
if (line.substring(line.length - 2) == '\r\n') // chomp it
line = line.substring(0, line.length - 2);
var candidate = {},
elems = line.split(' '),
i;
if (elems[6] != 'typ') {
console.log('did not find typ in the right place');
console.log(line);
return null;
}
candidate.foundation = elems[0].substring(12);
candidate.component = elems[1];
candidate.protocol = elems[2].toLowerCase();
candidate.priority = elems[3];
candidate.ip = elems[4];
candidate.port = elems[5];
// elems[6] => "typ"
candidate.type = elems[7];
for (i = 8; i < elems.length; i += 2) {
switch (elems[i]) {
case 'raddr':
candidate['rel-addr'] = elems[i + 1];
break;
case 'rport':
candidate['rel-port'] = elems[i + 1];
break;
case 'generation':
candidate.generation = elems[i + 1];
break;
default: // TODO
console.log('not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
}
}
candidate.network = '1';
candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
return candidate;
},
candidateFromJingle: function (cand) {
var line = 'a=candidate:';
line += cand.getAttribute('foundation');
line += ' ';
line += cand.getAttribute('component');
line += ' ';
line += cand.getAttribute('protocol'); //.toUpperCase(); // chrome M23 doesn't like this
line += ' ';
line += cand.getAttribute('priority');
line += ' ';
line += cand.getAttribute('ip');
line += ' ';
line += cand.getAttribute('port');
line += ' ';
line += 'typ';
line += ' ' + cand.getAttribute('type');
line += ' ';
switch (cand.getAttribute('type')) {
case 'srflx':
case 'prflx':
case 'relay':
if (cand.getAttribute('rel-addr') && cand.getAttribute('rel-port')) {
line += 'raddr';
line += ' ';
line += cand.getAttribute('rel-addr');
line += ' ';
line += 'rport';
line += ' ';
line += cand.getAttribute('rel-port');
line += ' ';
}
break;
}
line += 'generation';
line += ' ';
line += cand.getAttribute('generation') || '0';
return line + '\r\n';
}
};
/**
* Contains utility classes used in SDP class.
*
*/
/**
* Class holds a=ssrc lines and media type a=mid
* @param ssrc synchronization source identifier number(a=ssrc lines from SDP)
* @param type media type eg. "audio" or "video"(a=mid frm SDP)
* @constructor
*/
function ChannelSsrc(ssrc, type) {
this.ssrc = ssrc;
this.type = type;
this.lines = [];
}
/**
* Helper class represents media channel. Is a container for ChannelSsrc, holds channel idx and media type.
* @param channelNumber channel idx in SDP media array.
* @param mediaType media type(a=mid)
* @constructor
*/
function MediaChannel(channelNumber, mediaType) {
/**
* SDP channel number
* @type {*}
*/
this.chNumber = channelNumber;
/**
* Channel media type(a=mid)
* @type {*}
*/
this.mediaType = mediaType;
/**
* The maps of ssrc numbers to ChannelSsrc objects.
*/
this.ssrcs = {};
}
SDPUtil = {
iceparams: function (mediadesc, sessiondesc) {
var data = null;
if (SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc) &&
SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc)) {
data = {
ufrag: SDPUtil.parse_iceufrag(SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc)),
pwd: SDPUtil.parse_icepwd(SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc))
};
}
return data;
},
parse_iceufrag: function (line) {
return line.substring(12);
},
build_iceufrag: function (frag) {
return 'a=ice-ufrag:' + frag;
},
parse_icepwd: function (line) {
return line.substring(10);
},
build_icepwd: function (pwd) {
return 'a=ice-pwd:' + pwd;
},
parse_mid: function (line) {
return line.substring(6);
},
parse_mline: function (line) {
var parts = line.substring(2).split(' '),
data = {};
data.media = parts.shift();
data.port = parts.shift();
data.proto = parts.shift();
if (parts[parts.length - 1] === '') { // trailing whitespace
parts.pop();
}
data.fmt = parts;
return data;
},
build_mline: function (mline) {
return 'm=' + mline.media + ' ' + mline.port + ' ' + mline.proto + ' ' + mline.fmt.join(' ');
},
parse_rtpmap: function (line) {
var parts = line.substring(9).split(' '),
data = {};
data.id = parts.shift();
parts = parts[0].split('/');
data.name = parts.shift();
data.clockrate = parts.shift();
data.channels = parts.length ? parts.shift() : '1';
return data;
},
build_rtpmap: function (el) {
var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
line += '/' + el.getAttribute('channels');
}
return line;
},
parse_crypto: function (line) {
var parts = line.substring(9).split(' '),
data = {};
data.tag = parts.shift();
data['crypto-suite'] = parts.shift();
data['key-params'] = parts.shift();
if (parts.length) {
data['session-params'] = parts.join(' ');
}
return data;
},
parse_fingerprint: function (line) { // RFC 4572
var parts = line.substring(14).split(' '),
data = {};
data.hash = parts.shift();
data.fingerprint = parts.shift();
// TODO assert that fingerprint satisfies 2UHEX *(":" 2UHEX) ?
return data;
},
parse_fmtp: function (line) {
var parts = line.split(' '),
i, key, value,
data = [];
parts.shift();
parts = parts.join(' ').split(';');
for (i = 0; i < parts.length; i++) {
key = parts[i].split('=')[0];
while (key.length && key[0] == ' ') {
key = key.substring(1);
}
value = parts[i].split('=')[1];
if (key && value) {
data.push({name: key, value: value});
} else if (key) {
// rfc 4733 (DTMF) style stuff
data.push({name: '', value: key});
}
}
return data;
},
parse_icecandidate: function (line) {
var candidate = {},
elems = line.split(' ');
candidate.foundation = elems[0].substring(12);
candidate.component = elems[1];
candidate.protocol = elems[2].toLowerCase();
candidate.priority = elems[3];
candidate.ip = elems[4];
candidate.port = elems[5];
// elems[6] => "typ"
candidate.type = elems[7];
candidate.generation = 0; // default value, may be overwritten below
for (var i = 8; i < elems.length; i += 2) {
switch (elems[i]) {
case 'raddr':
candidate['rel-addr'] = elems[i + 1];
break;
case 'rport':
candidate['rel-port'] = elems[i + 1];
break;
case 'generation':
candidate.generation = elems[i + 1];
break;
default: // TODO
console.log('parse_icecandidate not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
}
}
candidate.network = '1';
candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
return candidate;
},
build_icecandidate: function (cand) {
var line = ['a=candidate:' + cand.foundation, cand.component, cand.protocol, cand.priority, cand.ip, cand.port, 'typ', cand.type].join(' ');
line += ' ';
switch (cand.type) {
case 'srflx':
case 'prflx':
case 'relay':
if (cand.hasOwnAttribute('rel-addr') && cand.hasOwnAttribute('rel-port')) {
line += 'raddr';
line += ' ';
line += cand['rel-addr'];
line += ' ';
line += 'rport';
line += ' ';
line += cand['rel-port'];
line += ' ';
}
break;
}
line += 'generation';
line += ' ';
line += cand.hasOwnAttribute('generation') ? cand.generation : '0';
return line;
},
parse_ssrc: function (desc) {
// proprietary mapping of a=ssrc lines
// TODO: see "Jingle RTP Source Description" by Juberti and P. Thatcher on google docs
// and parse according to that
var lines = desc.split('\r\n'),
data = {};
for (var i = 0; i < lines.length; i++) {
if (lines[i].substring(0, 7) == 'a=ssrc:') {
var idx = lines[i].indexOf(' ');
data[lines[i].substr(idx + 1).split(':', 2)[0]] = lines[i].substr(idx + 1).split(':', 2)[1];
}
}
return data;
},
parse_rtcpfb: function (line) {
var parts = line.substr(10).split(' ');
var data = {};
data.pt = parts.shift();
data.type = parts.shift();
data.params = parts;
return data;
},
parse_extmap: function (line) {
var parts = line.substr(9).split(' ');
var data = {};
data.value = parts.shift();
if (data.value.indexOf('/') != -1) {
data.direction = data.value.substr(data.value.indexOf('/') + 1);
data.value = data.value.substr(0, data.value.indexOf('/'));
} else {
data.direction = 'both';
}
data.uri = parts.shift();
data.params = parts;
return data;
},
find_line: function (haystack, needle, sessionpart) {
var lines = haystack.split('\r\n');
for (var i = 0; i < lines.length; i++) {
if (lines[i].substring(0, needle.length) == needle) {
return lines[i];
}
}
if (!sessionpart) {
return false;
}
// search session part
lines = sessionpart.split('\r\n');
for (var j = 0; j < lines.length; j++) {
if (lines[j].substring(0, needle.length) == needle) {
return lines[j];
}
}
return false;
},
find_lines: function (haystack, needle, sessionpart) {
var lines = haystack.split('\r\n'),
needles = [];
for (var i = 0; i < lines.length; i++) {
if (lines[i].substring(0, needle.length) == needle)
needles.push(lines[i]);
}
if (needles.length || !sessionpart) {
return needles;
}
// search session part
lines = sessionpart.split('\r\n');
for (var j = 0; j < lines.length; j++) {
if (lines[j].substring(0, needle.length) == needle) {
needles.push(lines[j]);
}
}
return needles;
},
candidateToJingle: function (line) {
// a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host generation 0
// <candidate component=... foundation=... generation=... id=... ip=... network=... port=... priority=... protocol=... type=.../>
if (line.substring(0, 12) != 'a=candidate:') {
console.log('parseCandidate called with a line that is not a candidate line');
console.log(line);
return null;
}
if (line.substring(line.length - 2) == '\r\n') // chomp it
line = line.substring(0, line.length - 2);
var candidate = {},
elems = line.split(' '),
i;
if (elems[6] != 'typ') {
console.log('did not find typ in the right place');
console.log(line);
return null;
}
candidate.foundation = elems[0].substring(12);
candidate.component = elems[1];
candidate.protocol = elems[2].toLowerCase();
candidate.priority = elems[3];
candidate.ip = elems[4];
candidate.port = elems[5];
// elems[6] => "typ"
candidate.type = elems[7];
for (i = 8; i < elems.length; i += 2) {
switch (elems[i]) {
case 'raddr':
candidate['rel-addr'] = elems[i + 1];
break;
case 'rport':
candidate['rel-port'] = elems[i + 1];
break;
case 'generation':
candidate.generation = elems[i + 1];
break;
default: // TODO
console.log('not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
}
}
candidate.network = '1';
candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
return candidate;
},
candidateFromJingle: function (cand) {
var line = 'a=candidate:';
line += cand.getAttribute('foundation');
line += ' ';
line += cand.getAttribute('component');
line += ' ';
line += cand.getAttribute('protocol'); //.toUpperCase(); // chrome M23 doesn't like this
line += ' ';
line += cand.getAttribute('priority');
line += ' ';
line += cand.getAttribute('ip');
line += ' ';
line += cand.getAttribute('port');
line += ' ';
line += 'typ';
line += ' ' + cand.getAttribute('type');
line += ' ';
switch (cand.getAttribute('type')) {
case 'srflx':
case 'prflx':
case 'relay':
if (cand.getAttribute('rel-addr') && cand.getAttribute('rel-port')) {
line += 'raddr';
line += ' ';
line += cand.getAttribute('rel-addr');
line += ' ';
line += 'rport';
line += ' ';
line += cand.getAttribute('rel-port');
line += ' ';
}
break;
}
line += 'generation';
line += ' ';
line += cand.getAttribute('generation') || '0';
return line + '\r\n';
}
};
/* jshint -W117 */ /* jshint -W117 */
// Jingle stuff // Jingle stuff
JingleSession.prototype = Object.create(SessionBase.prototype);
function JingleSession(me, sid, connection) { function JingleSession(me, sid, connection) {
SessionBase.call(this, connection, sid);
this.me = me; this.me = me;
this.sid = sid;
this.connection = connection;
this.initiator = null; this.initiator = null;
this.responder = null; this.responder = null;
this.isInitiator = null; this.isInitiator = null;
this.peerjid = null; this.peerjid = null;
this.state = null; this.state = null;
this.peerconnection = null;
this.remoteStream = null;
this.localSDP = null; this.localSDP = null;
this.remoteSDP = null; this.remoteSDP = null;
this.localStreams = []; this.localStreams = [];
...@@ -35,10 +35,6 @@ function JingleSession(me, sid, connection) { ...@@ -35,10 +35,6 @@ function JingleSession(me, sid, connection) {
this.reason = null; this.reason = null;
this.addssrc = [];
this.removessrc = [];
this.pendingop = null;
this.wait = true; this.wait = true;
} }
...@@ -54,16 +50,6 @@ JingleSession.prototype.initiate = function (peerjid, isInitiator) { ...@@ -54,16 +50,6 @@ JingleSession.prototype.initiate = function (peerjid, isInitiator) {
this.initiator = isInitiator ? this.me : peerjid; this.initiator = isInitiator ? this.me : peerjid;
this.responder = !isInitiator ? this.me : peerjid; this.responder = !isInitiator ? this.me : peerjid;
this.peerjid = peerjid; this.peerjid = peerjid;
//console.log('create PeerConnection ' + JSON.stringify(this.ice_config));
try {
this.peerconnection = new RTCPeerconnection(this.ice_config,
this.pc_constraints);
} catch (e) {
console.error('Failed to create PeerConnection, exception: ',
e.message);
console.error(e);
return;
}
this.hadstuncandidate = false; this.hadstuncandidate = false;
this.hadturncandidate = false; this.hadturncandidate = false;
this.lasticecandidate = false; this.lasticecandidate = false;
...@@ -71,13 +57,16 @@ JingleSession.prototype.initiate = function (peerjid, isInitiator) { ...@@ -71,13 +57,16 @@ JingleSession.prototype.initiate = function (peerjid, isInitiator) {
self.sendIceCandidate(event.candidate); self.sendIceCandidate(event.candidate);
}; };
this.peerconnection.onaddstream = function (event) { this.peerconnection.onaddstream = function (event) {
self.remoteStream = event.stream;
self.remoteStreams.push(event.stream); self.remoteStreams.push(event.stream);
$(document).trigger('remotestreamadded.jingle', [event, self.sid]); $(document).trigger('remotestreamadded.jingle', [event, self.sid]);
}; };
this.peerconnection.onremovestream = function (event) { this.peerconnection.onremovestream = function (event) {
self.remoteStream = null; // Remove the stream from remoteStreams
// FIXME: remove from this.remoteStreams var streamIdx = self.remoteStreams.indexOf(event.stream);
if(streamIdx !== -1){
self.remoteStreams.splice(streamIdx, 1);
}
// FIXME: remotestreamremoved.jingle not defined anywhere(unused)
$(document).trigger('remotestreamremoved.jingle', [event, self.sid]); $(document).trigger('remotestreamremoved.jingle', [event, self.sid]);
}; };
this.peerconnection.onsignalingstatechange = function (event) { this.peerconnection.onsignalingstatechange = function (event) {
...@@ -165,6 +154,22 @@ JingleSession.prototype.accept = function () { ...@@ -165,6 +154,22 @@ JingleSession.prototype.accept = function () {
); );
}; };
/**
* Implements SessionBase.sendSSRCUpdate.
*/
JingleSession.prototype.sendSSRCUpdate = function(sdpMediaSsrcs, fromJid, isadd) {
var self = this;
console.log('tell', self.peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from' + self.me);
if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')){
console.log("Too early to send updates");
return;
}
this.sendSSRCUpdateIq(sdpMediaSsrcs, self.sid, self.initiator, self.peerjid, isadd);
};
JingleSession.prototype.terminate = function (reason) { JingleSession.prototype.terminate = function (reason) {
this.state = 'ended'; this.state = 'ended';
this.reason = reason; this.reason = reason;
...@@ -633,158 +638,6 @@ JingleSession.prototype.sendTerminate = function (reason, text) { ...@@ -633,158 +638,6 @@ JingleSession.prototype.sendTerminate = function (reason, text) {
} }
}; };
JingleSession.prototype.addSource = function (elem) {
console.log('addssrc', new Date().getTime());
console.log('ice', this.peerconnection.iceConnectionState);
var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
var self = this;
$(elem).each(function (idx, content) {
var name = $(content).attr('name');
var lines = '';
tmp = $(content).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
tmp.each(function () {
var ssrc = $(this).attr('ssrc');
$(this).find('>parameter').each(function () {
lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
if ($(this).attr('value') && $(this).attr('value').length)
lines += ':' + $(this).attr('value');
lines += '\r\n';
});
});
sdp.media.forEach(function(media, idx) {
if (!SDPUtil.find_line(media, 'a=mid:' + name))
return;
sdp.media[idx] += lines;
if (!self.addssrc[idx]) self.addssrc[idx] = '';
self.addssrc[idx] += lines;
});
sdp.raw = sdp.session + sdp.media.join('');
});
this.modifySources();
};
JingleSession.prototype.removeSource = function (elem) {
console.log('removessrc', new Date().getTime());
console.log('ice', this.peerconnection.iceConnectionState);
var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
var self = this;
$(elem).each(function (idx, content) {
var name = $(content).attr('name');
var lines = '';
tmp = $(content).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
tmp.each(function () {
var ssrc = $(this).attr('ssrc');
$(this).find('>parameter').each(function () {
lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
if ($(this).attr('value') && $(this).attr('value').length)
lines += ':' + $(this).attr('value');
lines += '\r\n';
});
});
sdp.media.forEach(function(media, idx) {
if (!SDPUtil.find_line(media, 'a=mid:' + name))
return;
sdp.media[idx] += lines;
if (!self.removessrc[idx]) self.removessrc[idx] = '';
self.removessrc[idx] += lines;
});
sdp.raw = sdp.session + sdp.media.join('');
});
this.modifySources();
};
JingleSession.prototype.modifySources = function() {
var self = this;
if (this.peerconnection.signalingState == 'closed') return;
if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null)) return;
if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')) {
console.warn('modifySources not yet', this.peerconnection.signalingState, this.peerconnection.iceConnectionState);
this.wait = true;
window.setTimeout(function() { self.modifySources(); }, 250);
return;
}
if (this.wait) {
window.setTimeout(function() { self.modifySources(); }, 2500);
this.wait = false;
return;
}
var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
// add sources
this.addssrc.forEach(function(lines, idx) {
sdp.media[idx] += lines;
});
this.addssrc = [];
// remove sources
this.removessrc.forEach(function(lines, idx) {
lines = lines.split('\r\n');
lines.pop(); // remove empty last element;
lines.forEach(function(line) {
sdp.media[idx] = sdp.media[idx].replace(line + '\r\n', '');
});
});
this.removessrc = [];
sdp.raw = sdp.session + sdp.media.join('');
this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: sdp.raw}),
function() {
self.peerconnection.createAnswer(
function(modifiedAnswer) {
// change video direction, see https://github.com/jitsi/jitmeet/issues/41
if (self.pendingop !== null) {
var sdp = new SDP(modifiedAnswer.sdp);
if (sdp.media.length > 1) {
switch(self.pendingop) {
case 'mute':
sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
break;
case 'unmute':
sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
break;
}
sdp.raw = sdp.session + sdp.media.join('');
modifiedAnswer.sdp = sdp.raw;
}
self.pendingop = null;
}
self.peerconnection.setLocalDescription(modifiedAnswer,
function() {
//console.log('modified setLocalDescription ok');
$(document).trigger('setLocalDescription.jingle', [self.sid]);
},
function(error) {
console.log('modified setLocalDescription failed');
}
);
},
function(error) {
console.log('modified answer failed');
}
);
},
function(error) {
console.log('modify failed');
}
);
};
// SDP-based mute by going recvonly/sendrecv
// FIXME: should probably black out the screen as well
JingleSession.prototype.hardMuteVideo = function (muted) {
this.pendingop = muted ? 'mute' : 'unmute';
this.modifySources();
this.connection.jingle.localStream.getVideoTracks().forEach(function (track) {
track.enabled = !muted;
});
};
JingleSession.prototype.sendMute = function (muted, content) { JingleSession.prototype.sendMute = function (muted, content) {
var info = $iq({to: this.peerjid, var info = $iq({to: this.peerjid,
type: 'set'}) type: 'set'})
......
/**
* Base class for ColibriFocus and JingleSession.
* @param connection Strophe connection object
* @param sid my session identifier(resource)
* @constructor
*/
function SessionBase(connection, sid){
this.connection = connection;
this.sid = sid;
this.peerconnection
= new TraceablePeerConnection(
connection.jingle.ice_config,
connection.jingle.pc_constraints);
}
SessionBase.prototype.modifySources = function (successCallback) {
var self = this;
this.peerconnection.modifySources(function(){
$(document).trigger('setLocalDescription.jingle', [self.sid]);
if(successCallback) {
successCallback();
}
});
};
SessionBase.prototype.addSource = function (elem, fromJid) {
this.peerconnection.addSource(elem);
this.modifySources();
};
SessionBase.prototype.removeSource = function (elem, fromJid) {
this.peerconnection.removeSource(elem);
this.modifySources();
};
/**
* Switches video streams.
* @param new_stream new stream that will be used as video of this session.
* @param oldStream old video stream of this session.
* @param success_callback callback executed after successful stream switch.
*/
SessionBase.prototype.switchStreams = function (new_stream, oldStream, success_callback) {
var self = this;
// Remember SDP to figure out added/removed SSRCs
var oldSdp = null;
if(self.peerconnection.localDescription) {
oldSdp = new SDP(self.peerconnection.localDescription.sdp);
}
// Stop the stream to trigger onended event for old stream
oldStream.stop();
self.peerconnection.removeStream(oldStream);
self.connection.jingle.localVideo = new_stream;
self.peerconnection.addStream(self.connection.jingle.localVideo);
self.connection.jingle.localStreams = [];
self.connection.jingle.localStreams.push(self.connection.jingle.localAudio);
self.connection.jingle.localStreams.push(self.connection.jingle.localVideo);
// Conference is not active
if(!oldSdp) {
success_callback();
return;
}
self.peerconnection.switchstreams = true;
self.modifySources(function() {
console.log('modify sources done');
var newSdp = new SDP(self.peerconnection.localDescription.sdp);
console.log("SDPs", oldSdp, newSdp);
self.notifyMySSRCUpdate(oldSdp, newSdp);
success_callback();
});
};
/**
* Figures out added/removed ssrcs and send update IQs.
* @param old_sdp SDP object for old description.
* @param new_sdp SDP object for new description.
*/
SessionBase.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) {
var old_media = old_sdp.getMediaSsrcMap();
var new_media = new_sdp.getMediaSsrcMap();
//console.log("old/new medias: ", old_media, new_media);
var toAdd = old_sdp.getNewMedia(new_sdp);
var toRemove = new_sdp.getNewMedia(old_sdp);
//console.log("to add", toAdd);
//console.log("to remove", toRemove);
if(Object.keys(toRemove).length > 0){
this.sendSSRCUpdate(toRemove, null, false);
}
if(Object.keys(toAdd).length > 0){
this.sendSSRCUpdate(toAdd, null, true);
}
};
/**
* Empty method that does nothing by default. It should send SSRC update IQs to session participants.
* @param sdpMediaSsrcs array of
* @param fromJid
* @param isAdd
*/
SessionBase.prototype.sendSSRCUpdate = function(sdpMediaSsrcs, fromJid, isAdd) {
//FIXME: put default implementation here(maybe from JingleSession?)
}
/**
* Sends SSRC update IQ.
* @param sdpMediaSsrcs SSRCs map obtained from SDP.getNewMedia. Cntains SSRCs to add/remove.
* @param sid session identifier that will be put into the IQ.
* @param initiator initiator identifier.
* @param toJid destination Jid
* @param isAdd indicates if this is remove or add operation.
*/
SessionBase.prototype.sendSSRCUpdateIq = function(sdpMediaSsrcs, sid, initiator, toJid, isAdd) {
var self = this;
var modify = $iq({to: toJid, type: 'set'})
.c('jingle', {
xmlns: 'urn:xmpp:jingle:1',
action: isAdd ? 'addsource' : 'removesource',
initiator: initiator,
sid: sid
}
);
// FIXME: only announce video ssrcs since we mix audio and dont need
// the audio ssrcs therefore
var modified = false;
Object.keys(sdpMediaSsrcs).forEach(function(channelNum){
modified = true;
var channel = sdpMediaSsrcs[channelNum];
modify.c('content', {name: channel.mediaType});
// FIXME: not completly sure this operates on blocks and / or handles different ssrcs correctly
// generate sources from lines
Object.keys(channel.ssrcs).forEach(function(ssrcNum) {
var mediaSsrc = channel.ssrcs[ssrcNum];
modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
modify.attrs({ssrc: mediaSsrc.ssrc});
// iterate over ssrc lines
mediaSsrc.lines.forEach(function (line) {
var idx = line.indexOf(' ');
var kv = line.substr(idx + 1);
modify.c('parameter');
if (kv.indexOf(':') == -1) {
modify.attrs({ name: kv });
} else {
modify.attrs({ name: kv.split(':', 2)[0] });
modify.attrs({ value: kv.split(':', 2)[1] });
}
modify.up(); // end of parameter
});
modify.up(); // end of source
});
modify.up(); // end of content
});
if (modified) {
self.connection.sendIQ(modify,
function (res) {
console.info('got modify result', res);
},
function (err) {
console.error('got modify error', err);
}
);
} else {
console.log('modification not necessary');
}
};
// SDP-based mute by going recvonly/sendrecv
// FIXME: should probably black out the screen as well
SessionBase.prototype.toggleVideoMute = function (callback) {
var ismuted = false;
var localVideo = connection.jingle.localVideo;
for (var idx = 0; idx < localVideo.getVideoTracks().length; idx++) {
ismuted = !localVideo.getVideoTracks()[idx].enabled;
}
for (var idx = 0; idx < localVideo.getVideoTracks().length; idx++) {
localVideo.getVideoTracks()[idx].enabled = !localVideo.getVideoTracks()[idx].enabled;
}
this.peerconnection.hardMuteVideo(!ismuted);
this.modifySources(callback(!ismuted));
};
\ No newline at end of file
...@@ -232,6 +232,14 @@ Strophe.addConnectionPlugin('emuc', { ...@@ -232,6 +232,14 @@ Strophe.addConnectionPlugin('emuc', {
this.presMap['source' + sourceNumber + '_ssrc'] = ssrcs; this.presMap['source' + sourceNumber + '_ssrc'] = ssrcs;
this.presMap['source' + sourceNumber + '_direction'] = direction; this.presMap['source' + sourceNumber + '_direction'] = direction;
}, },
clearPresenceMedia: function () {
var self = this;
Object.keys(this.presMap).forEach( function(key) {
if(key.indexOf('source') != -1) {
delete self.presMap[key];
}
});
},
addPreziToPresence: function (url, currentSlide) { addPreziToPresence: function (url, currentSlide) {
this.presMap['prezins'] = 'http://jitsi.org/jitmeet/prezi'; this.presMap['prezins'] = 'http://jitsi.org/jitmeet/prezi';
this.presMap['preziurl'] = url; this.presMap['preziurl'] = url;
......
<html> <html>
<head> <head>
<title></title> <title>JitMeet: Unsupported Browser</title>
<link rel="stylesheet" type="text/css" media="screen" href="css/chromeonly.css" /> <link rel="stylesheet" type="text/css" media="screen" href="css/chromeonly.css" />
</head> </head>
<body> <body>
<!-- wrap starts here --> <!-- wrap starts here -->
<div id="wrap"> <div id="wrap">
<a href="http://google.com/chrome"><div id="left"></div></a> <a href="http://google.com/chrome"><div id="left"></div></a>
<div id="middle"></div> <div id="middle"></div>
<div id="text"> <div id="text">
<p>This service only works with Chrome.</p> <p>This application is currently only supported by <a href="http://google.com/chrome">Chrome</a>, <a href="http://www.chromium.org/">Chromium</a> and <a href="http://www.opera.com">Opera</a></p>
<p><a href="http://google.com/chrome">Download Chrome</a></p> <p><a href="http://google.com/chrome">Download Chrome</a></p>
</div> <p class="firefox">We are hoping that <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=977864">multistream support</a> for Firefox would not be long so that we could all use this application with our favorite browser.</p>
<!-- wrap ends here --> </div>
</div> <!-- wrap ends here -->
</body> </div>
</body>
</html> </html>
...@@ -89,6 +89,33 @@ html, body{ ...@@ -89,6 +89,33 @@ html, body{
border-left:1px solid #424242; border-left:1px solid #424242;
} }
#chat {
font-size:1.65em;
-webkit-transition: all .5s ease-in-out;;
-moz-transition: all .5s ease-in-out;;
transition: all .5s ease-in-out;;
}
#chat.active {
-webkit-text-shadow: 0 0 10px #ffffff;
-moz-text-shadow: 0 0 10px #ffffff;
text-shadow: 0 0 10px #ffffff;
/* -webkit-transform: scale(1.1); */
}
.toolbar_span {
display: inline-block;
position: relative;
}
.toolbar_span>span {
display: inline-block;
position: absolute;
font-size: 7pt;
color: #ffffff;
text-align:center;
cursor: pointer;
}
.localuser { .localuser {
color: #087dba; color: #087dba;
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
<script src="js/webrtc.sdp.js"></script> <script src="js/webrtc.sdp.js"></script>
<script src="js/strophe-openfire.js"></script> <script src="js/strophe-openfire.js"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
<script src="js/util.js"></script>
<link rel="shortcut icon" href="favicon.ico"/> <link rel="shortcut icon" href="favicon.ico"/>
<link rel="stylesheet" href="font-awesome-4.0.3/css/font-awesome.css"> <link rel="stylesheet" href="font-awesome-4.0.3/css/font-awesome.css">
...@@ -36,7 +37,10 @@ ...@@ -36,7 +37,10 @@
<div class="header_button_separator"></div> <div class="header_button_separator"></div>
<a class="button" onclick="openLinkDialog();"><i title="Invite others" class="fa fa-link fa-lg"></i></a> <a class="button" onclick="openLinkDialog();"><i title="Invite others" class="fa fa-link fa-lg"></i></a>
<div class="header_button_separator"></div> <div class="header_button_separator"></div>
<a class="button" onclick='openChat();'><i id="chat" title="Open chat" class="fa fa-comments fa-lg"></i></a> <span class="toolbar_span">
<a class="button" onclick='openChat();'><i id="chat" title="Open chat" class="fa fa-comments-o fa-lg"></i></a>
<span id="unreadMessages"></span>
</span>
<div class="header_button_separator"></div> <div class="header_button_separator"></div>
<a class="button" onclick='openPDFDialog();'><i id="pdf" title="Share PDF" class="fa fa-file fa-lg"></i></a> <a class="button" onclick='openPDFDialog();'><i id="pdf" title="Share PDF" class="fa fa-file fa-lg"></i></a>
<div class="header_button_separator"></div> <div class="header_button_separator"></div>
......
...@@ -9,6 +9,8 @@ var pdfFrame = null; ...@@ -9,6 +9,8 @@ var pdfFrame = null;
var pdfPage = "1"; var pdfPage = "1";
var altView = false; var altView = false;
var sipUri = null; var sipUri = null;
var notificationInterval = false;
var unreadMessages = 0;
$(document).ready(function () $(document).ready(function ()
{ {
...@@ -55,12 +57,16 @@ $(document).ready(function () ...@@ -55,12 +57,16 @@ $(document).ready(function ()
$('#usermsg').keydown(function(event) $('#usermsg').keydown(function(event)
{ {
if (event.keyCode == 13) { if (event.keyCode == 13)
{
event.preventDefault(); event.preventDefault();
var message = this.value; var message = this.value;
$('#usermsg').val('').trigger('autosize.resize'); $('#usermsg').val('').trigger('autosize.resize');
this.focus(); this.focus();
connection.emuc.sendMessage(message, nickname); connection.emuc.sendMessage(message, nickname);
unreadMessages = 0;
setVisualNotification(false);
} }
}); });
...@@ -376,7 +382,10 @@ function getConstraints(um, resolution, bandwidth, fps) ...@@ -376,7 +382,10 @@ function getConstraints(um, resolution, bandwidth, fps)
if (um.indexOf('screen') >= 0) { if (um.indexOf('screen') >= 0) {
window.RTC.rayo.constraints.video = { window.RTC.rayo.constraints.video = {
"mandatory": { "mandatory": {
"chromeMediaSource": "screen" "chromeMediaSource": "screen",
"maxWidth": window.screen.width,
"maxHeight": window.screen.height,
"maxFrameRate": "3"
} }
}; };
} }
...@@ -1008,6 +1017,10 @@ function updateChatConversation(nick, message) ...@@ -1008,6 +1017,10 @@ function updateChatConversation(nick, message)
if (nickname == nick) if (nickname == nick)
divClassName = "In"; divClassName = "In";
else {
unreadMessages++;
setVisualNotification(true);
}
var content = '<div class="message message' + divClassName + '">' var content = '<div class="message message' + divClassName + '">'
+'<span class="msgText">' + setEmoticons(message) + '</span>' +'<span class="msgText">' + setEmoticons(message) + '</span>'
...@@ -1378,7 +1391,38 @@ function linkify(inputText) ...@@ -1378,7 +1391,38 @@ function linkify(inputText)
return replacedText; return replacedText;
} }
Date.prototype.format = function(format) { function setVisualNotification(show)
{
var unreadMsgElement = document.getElementById('unreadMessages');
if (unreadMessages) {
unreadMsgElement.innerHTML = unreadMessages.toString();
var chatButtonElement = document.getElementById('chat').parentNode;
var leftIndent = (Util.getTextWidth(chatButtonElement) - Util.getTextWidth(unreadMsgElement) - 5)/2;
var topIndent = (Util.getTextHeight(chatButtonElement) - Util.getTextHeight(unreadMsgElement))/2 - 2;
unreadMsgElement.setAttribute('style', 'top:' + topIndent + '; left:' + leftIndent +';');
}
else
unreadMsgElement.innerHTML = '';
var glower = $('#chat');
if (show && !notificationInterval) {
notificationInterval = window.setInterval(function() {
glower.toggleClass('active');
}, 800);
}
else if (!show && notificationInterval) {
window.clearInterval(notificationInterval);
notificationInterval = false;
glower.removeClass('active');
}
}
Date.prototype.format = function(format)
{
var returnStr = ''; var returnStr = '';
var replace = Date.replaceChars; var replace = Date.replaceChars;
for (var i = 0; i < format.length; i++) { for (var i = 0; i < format.length; i++) {
......
...@@ -182,9 +182,9 @@ function getConstraints(um, resolution, bandwidth, fps) ...@@ -182,9 +182,9 @@ function getConstraints(um, resolution, bandwidth, fps)
window.RTC.rayo.constraints.video = { window.RTC.rayo.constraints.video = {
"mandatory": { "mandatory": {
"chromeMediaSource": "screen", "chromeMediaSource": "screen",
"maxWidth": "1280", "maxWidth": window.screen.width,
"maxHeight": "1280", "maxHeight": window.screen.height,
"maxFrameRate": "30" "maxFrameRate": "3"
} }
}; };
} }
......
/**
* Utility functions.
*/
var Util = (function (my) {
/**
* Returns the text width for the given element.
*
* @param el the element
*/
my.getTextWidth = function(el) {
return (el.clientWidth + 1);
};
/**
* Returns the text height for the given element.
*
* @param el the element
*/
my.getTextHeight = function(el) {
return (el.clientHeight + 1);
};
/**
* Casts the given number to integer.
*
* @param number the number to cast
*/
my.toInteger = function(number) {
return Math.round(Number(number));
};
/**
* Plays the sound given by id.
*
* @param id the identifier of the audio element.
*/
my.playSoundNotification = function(id) {
document.getElementById(id).play();
};
/**
* Escapes the given text.
*/
my.escapeHtml = function(unsafeText) {
return $('<div/>').text(unsafeText).html();
};
/**
* Indicates if the given string is an alphanumeric string.
* Note that some special characters are also allowed (-, _ , /) for the
* purpose of checking URIs. (FIXME: This should maybe moved to another not
* so generic method in the future.)
*/
my.isAlphanumeric = function(unsafeText) {
var regex = /^[a-z0-9-_\/]+$/i;
return regex.test(unsafeText);
};
return my;
}(Util || {}));
\ No newline at end of file
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/*
* Element.java
*
*
*/
package org.ebml;
/**
* Defines the basic EBML element. Subclasses may provide child element
* access.
* Created on November 19, 2002, 9:11 PM
* @author John Cannon
*/
public class BinaryElement extends Element {
/*private byte[] type = {
0x00};*/
private static int MIN_SIZE_LENGTH = 4;
//private long size = 0;
//protected byte[] data;
/*
* Creates a new instance of Element
@param type The type ID of this element
*/
public BinaryElement(byte[] type) {
super(type);
}
/** Getter for property data.
* @return Value of property data.
*
*/
public byte[] getData() {
return this.data;
}
/** Setter for property data.
* @param data New value of property data.
*
*/
public void setData(byte[] data) {
this.data = data;
this.size = data.length;
}
/** Getter for property size.
* @return Value of property size.
*
*/
public long getSize() {
return size;
}
/** Setter for property size.
* @param size New value of property size.
*
*/
public void setSize(long size) {
this.size = size;
}
/** Getter for property type.
* @return Value of property type.
*
*/
public byte[] getType() {
return type;
}
/** Setter for property type.
* @param type New value of property type.
*
*/
public void setType(byte[] type) {
this.type = type;
}
public byte[] toByteArray() {
byte[] head = makeEbmlCode(type, size);
byte[] ret = new byte[head.length + data.length];
org.ebml.util.ArrayCopy.arraycopy(head, 0, ret, 0, head.length);
org.ebml.util.ArrayCopy.arraycopy(data, 0, ret, head.length, data.length);
return ret;
}
public static void setMinSizeLength(int minSize) {
MIN_SIZE_LENGTH = minSize;
}
public static int getMinSizeLength() {
return MIN_SIZE_LENGTH;
}
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml;
import java.util.*;
public class DateElement extends SignedIntegerElement {
//const uint64 EbmlDate::UnixEpochDelay = 978307200; // 2001/01/01 00:00:00 UTC
public static long UnixEpochDelay = 978307200; // 2001/01/01 00:00:00 UTC
private static int MIN_SIZE_LENGTH = 8;
public DateElement(byte[] type) {
super(type);
}
/**
* Set the Date of this element
* @param value Date to set
*/
public void setDate(Date value) {
long val = (value.getTime() - UnixEpochDelay) * 1000000000;
setData(packInt(val, MIN_SIZE_LENGTH));
}
/**
* Get the Date value of this element
* @return Date of this element
*/
public Date getDate() {
/*
Date begin = new Date(0);
Date start = new Date(1970, 1, 1, 0, 0, 0);
Date end = new Date(2001, 1, 1, 0, 0, 0);
long diff0 = begin.getTime();
long diff1 = start.getTime();
long diff2 = end.getTime();
long diff3 = Date.UTC(2001, 1, 1, 0, 0, 0) - Date.UTC(1970, 1, 1, 0, 0, 0);
*/
long val = getValue();;
val = val / 1000000000 + UnixEpochDelay;
return new Date(val);
}
/**
* It's not recommended to use this method.
* Use the setDate(Date) method instead.
*/
public void setValue(long value)
{
setData(packInt(value, MIN_SIZE_LENGTH));
}
}
\ No newline at end of file
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml;
public interface DocType {
public ElementType getElements();
public Element createElement(ElementType type);
public Element createElement(byte [] type);
}
\ No newline at end of file
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml;
import org.ebml.io.*;
import org.ebml.util.*;
/**
* EBMLReader.java
*
* Created on November 18, 2002, 4:03 PM
*
* @version 1.0
*/
/**
* <h1>JEBML Intro</h1>
* <hr>
* <p>The following are the basic steps of how reading in JEBML works.</p>
* <ul>
* <li>1. The EbmlReader class reads the element header (id+size) and
* looks it up in the supplied DocType class.
* <li>2. The correct element type (Binary, UInteger, String, etc) is
* created using the DocType data, BinaryElement is default
* element type for unknown elements.
* <li>3. The MatroskaDocType has the ids of all the elements staticly
* declared.
* <br>So to easily find out what an element is, you can use some code
* like the following code
* <p>
* <code>
* Element level1; <br>
* // ... fill level1 <br>
* if (level1.equals(MatroskaDocType.SegmentInfo_Id)) { <br>
* // Your Code Here <br>
* } <br>
* </code>
* </p>
* <li>4. To get the actual data for an Element you call the readData
* method, if you just want to skip it use skipData().
* <li>5. MasterElements are special, they have the readNextChild()
* method which returns the next child element, returning null
* when all the children have been read (it keeps track of the
* current inputstream position).
* <li>The usage method for JEBML is very close to libebml/libmatroska.
* </ul>
* <hr>
*
* Reads EBML elements from a <code>DataSource</code> and looks them up
* in the provided <code>DocType</code>.
*
* @author (c) 2002 John Cannon
* @author (c) 2004 Jory Stone
*/
public class EBMLReader {
protected DataSource source;
protected DocType doc;
protected ElementType elementTypes;
protected ElementType lastElementType;
/** Creates a new <code>EBMLReader</code> reading from the <code>DataSource
* source</code>. The <code>DocType doc</code> is used to validate the
* document.
*
* @param source DataSource to read from
* @param doc DocType to use to validate the docment
*/
public EBMLReader(DataSource source, DocType doc) {
this.source = source;
this.doc = doc;
this.elementTypes = doc.getElements();
}
public Element readNextElement() {
//Read the type.
byte[] elementType = readEBMLCodeAsBytes(source);
if (elementType == null)
// Failed to read type id
return null;
//Read the size.
byte[] data = getEBMLCodeAsBytes(source);
long elementSize = parseEBMLCode(data);
if (elementSize == 0)
// Zero sized element is valid
;//return null;
Element elem = null;
//lastElementType = elementTypes.findElement(elementType);
//if (lastElementType == null) {
// lastElementType = new UnknownElementType(elementType);
//}
//elem = doc.createElement(lastElementType);
elem = doc.createElement(elementType);
if (elem == null) {
return null;
}
//Set it's size
elem.setSize(elementSize);
elem.setHeaderSize(data.length);
//Setup a buffer for it's data
//byte[] elementData = new byte[(int)elementSize];
//Read the data
//source.read(elementData, 0, elementData.length);
//Set the data property on the element
//elem.setData(elementData);
//System.out.println("EBMLReader.readNextElement() returning element " + elem.getElementType().name + " with size " + Long.toString(elem.getTotalSize()-elementSize)+" "+Long.toString(elementSize));
//Return the element
return elem;
}
static public byte[] getEBMLCodeAsBytes(DataSource source) {
//Begin loop with byte set to newly read byte.
byte firstByte = source.readByte();
int numBytes = 0;
//Begin by counting the bits unset before the first '1'.
long mask = 0x0080;
for (int i = 0; i < 8; i++) {
//Start at left, shift to right.
if ((firstByte & mask) == mask) { //One found
//Set number of bytes in size = i+1 ( we must count the 1 too)
numBytes = i + 1;
//exit loop by pushing i out of the limit
i = 8;
}
mask >>>= 1;
}
if (numBytes == 0)
// Invalid size
return null;
//Setup space to store the bits
byte[] data = new byte[numBytes];
//Clear the 1 at the front of this byte, all the way to the beginning of the size
data[0] = (byte)(firstByte & ((0xFF >>> (numBytes))));
if (numBytes > 1) {
//Read the rest of the size.
source.read(data, 1, numBytes - 1);
}
return data;
}
/**
* Reads an (Unsigned) EBML code from the DataSource and encodes it into a long. This size should be
* cast into an int for actual use as Java only allows upto 32-bit file I/O operations.
*
* @return ebml size
*/
static public long readEBMLCode(DataSource source) {
//Begin loop with byte set to newly read byte.
byte firstByte = source.readByte();
int numBytes = 0;
//Begin by counting the bits unset before the first '1'.
long mask = 0x0080;
for (int i = 0; i < 8; i++) {
//Start at left, shift to right.
if ((firstByte & mask) == mask) { //One found
//Set number of bytes in size = i+1 ( we must count the 1 too)
numBytes = i + 1;
//exit loop by pushing i out of the limit
i = 8;
}
mask >>>= 1;
}
if (numBytes == 0)
// Invalid size
return 0;
//Setup space to store the bits
byte[] data = new byte[numBytes];
//Clear the 1 at the front of this byte, all the way to the beginning of the size
data[0] = (byte)(firstByte & ((0xFF >>> (numBytes))));
if (numBytes > 1) {
//Read the rest of the size.
source.read(data, 1, numBytes - 1);
}
//Put this into a long
long size = 0;
long n = 0;
for (int i = 0; i < numBytes; i++) {
n = ((long)data[numBytes - 1 - i] << 56) >>> 56;
size = size | (n << (8 * i));
}
return size;
}
public static long parseEBMLCode(byte[] data) {
if(data==null)
return 0;
//Put this into a long
long size = 0;
long n = 0;
for (int i = 0; i < data.length; i++) {
n = ((long)data[data.length - 1 - i] << 56) >>> 56;
size = size | (n << (8 * i));
}
return size;
}
/**
* Reads an (Unsigned) EBML code from the DataSource and encodes it into a long. This size should be
* cast into an int for actual use as Java only allows upto 32-bit file I/O operations.
*
* @return ebml size
*/
static public long readEBMLCode(byte [] source)
{
return readEBMLCode(source, 0);
}
/**
* Reads an (Unsigned) EBML code from the DataSource and encodes it into a long. This size should be
* cast into an int for actual use as Java only allows upto 32-bit file I/O operations.
*
* @return ebml size
*/
static public long readEBMLCode(byte [] source, int offset)
{
//Begin loop with byte set to newly read byte.
byte firstByte = source[offset];
int numBytes = 0;
//Begin by counting the bits unset before the first '1'.
long mask = 0x0080;
for (int i = 0; i < 8; i++)
{
//Start at left, shift to right.
if ((firstByte & mask) == mask)
{ //One found
//Set number of bytes in size = i+1 ( we must count the 1 too)
numBytes = i + 1;
//exit loop by pushing i out of the limit
i = 8;
}
mask >>>= 1;
}
if (numBytes == 0)
// Invalid size
return 0;
//Setup space to store the bits
byte[] data = new byte[numBytes];
//Clear the 1 at the front of this byte, all the way to the beginning of the size
data[0] = (byte)(firstByte & ((0xFF >>> (numBytes))));
if (numBytes > 1)
{
//Read the rest of the size.
ArrayCopy.arraycopy(data, 1, source, offset+1, numBytes - 1);
}
//Put this into a long
long size = 0;
long n = 0;
for (int i = 0; i < numBytes; i++)
{
n = ((long)data[numBytes - 1 - i] << 56) >>> 56;
size = size | (n << (8 * i));
}
return size;
}
/**
* Reads an Signed EBML code from the DataSource and encodes it into a long. This size should be
* cast into an int for actual use as Java only allows upto 32-bit file I/O operations.
*
* @return ebml size
*/
static public long readSignedEBMLCode(byte [] source)
{
return readSignedEBMLCode(source, 0);
}
/**
* Reads an Signed EBML code from the DataSource and encodes it into a long. This size should be
* cast into an int for actual use as Java only allows upto 32-bit file I/O operations.
*
* @return ebml size
*/
static public long readSignedEBMLCode(byte [] source, int offset)
{
//Begin loop with byte set to newly read byte.
byte firstByte = source[offset];
int numBytes = 0;
//Begin by counting the bits unset before the first '1'.
long mask = 0x0080;
for (int i = 0; i < 8; i++)
{
//Start at left, shift to right.
if ((firstByte & mask) == mask)
{ //One found
//Set number of bytes in size = i+1 ( we must count the 1 too)
numBytes = i + 1;
//exit loop by pushing i out of the limit
i = 8;
}
mask >>>= 1;
}
if (numBytes == 0)
// Invalid size
return 0;
//Setup space to store the bits
byte[] data = new byte[numBytes];
//Clear the 1 at the front of this byte, all the way to the beginning of the size
data[0] = (byte)(firstByte & ((0xFF >>> (numBytes))));
if (numBytes > 1)
{
//Read the rest of the size.
ArrayCopy.arraycopy(data, 1, source, offset+1, numBytes - 1);
}
//Put this into a long
long size = 0;
long n = 0;
for (int i = 0; i < numBytes; i++)
{
n = ((long)data[numBytes - 1 - i] << 56) >>> 56;
size = size | (n << (8 * i));
}
// Sign it ;)
if (numBytes == 1)
{
size -= 63;
}
else if (numBytes == 2)
{
size -= 8191;
}
else if (numBytes == 3)
{
size -= 1048575;
}
else if (numBytes == 4)
{
size -= 134217727;
}
return size;
}
/**
* Reads an Signed EBML code from the DataSource and encodes it into a long. This size should be
* cast into an int for actual use as Java only allows upto 32-bit file I/O operations.
*
* @return ebml size
*/
static public long readSignedEBMLCode(DataSource source) {
//Begin loop with byte set to newly read byte.
byte firstByte = source.readByte();
int numBytes = 0;
//Begin by counting the bits unset before the first '1'.
long mask = 0x0080;
for (int i = 0; i < 8; i++) {
//Start at left, shift to right.
if ((firstByte & mask) == mask) { //One found
//Set number of bytes in size = i+1 ( we must count the 1 too)
numBytes = i + 1;
//exit loop by pushing i out of the limit
i = 8;
}
mask >>>= 1;
}
if (numBytes == 0)
// Invalid size
return 0;
//Setup space to store the bits
byte[] data = new byte[numBytes];
//Clear the 1 at the front of this byte, all the way to the beginning of the size
data[0] = (byte)(firstByte & ((0xFF >>> (numBytes))));
if (numBytes > 1) {
//Read the rest of the size.
source.read(data, 1, numBytes - 1);
}
//Put this into a long
long size = 0;
long n = 0;
for (int i = 0; i < numBytes; i++) {
n = ((long)data[numBytes - 1 - i] << 56) >>> 56;
size = size | (n << (8 * i));
}
// Sign it ;)
if (numBytes == 1) {
size -= 63;
} else if (numBytes == 2) {
size -= 8191;
} else if (numBytes == 3) {
size -= 1048575;
} else if (numBytes == 4) {
size -= 134217727;
}
return size;
}
/**
*Reads an EBML code from the DataSource.
*
* @return byte array filled with the ebml size, (size bits included)
*/
static public byte[] readEBMLCodeAsBytes(DataSource source) {
//Begin loop with byte set to newly read byte.
byte firstByte = source.readByte();
int numBytes = 0;
//Begin by counting the bits unset before the first '1'.
long mask = 0x0080;
for (int i = 0; i < 8; i++) {
//Start at left, shift to right.
if ((firstByte & mask) == mask) { //One found
//Set number of bytes in size = i+1 ( we must count the 1 too)
numBytes = i + 1;
//exit loop by pushing i out of the limit
i = 8;
}
mask >>>= 1;
}
if (numBytes == 0)
// Invalid element
return null;
//Setup space to store the bits
byte[] data = new byte[numBytes];
//Clear the 1 at the front of this byte, all the way to the beginning of the size
data[0] = (byte)((firstByte));// & ((0xFF >>> (numBytes))));
if (numBytes > 1) {
//Read the rest of the size.
source.read(data, 1, numBytes - 1);
}
return data;
}
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml;
import org.ebml.io.*;
/**
* Summary description for EBMLWriter.
*/
public class EBMLWriter
{
protected DataWriter writer;
/** Creates a new <code>EBMLReader</code> reading from the <code>DataSource
* source</code>. The <code>DocType doc</code> is used to validate the
* document.
*
* @param source DataSource to read from
* @param doc DocType to use to validate the docment
*/
public EBMLWriter(DataWriter writer)
{
this.writer = writer;
}
public long writeElement(Element elem)
{
return elem.writeHeaderData(writer) + elem.writeData(writer);
}
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml;
/*
* Element.java
*
* Created on November 19, 2002, 9:11 PM
*/
import org.ebml.io.*;
import org.ebml.util.*;
/**
* Defines the basic EBML element. Subclasses may provide child element access.
* @author John Cannon
*/
public class Element {
protected Element parent;
protected ElementType typeInfo;
protected byte[] type = {
0x00};
private static int MIN_SIZE_LENGTH = 0;
protected long size = 0;
protected byte[] data;
protected boolean dataRead = false;
private int headerSize;
/** Creates a new instance of Element */
public Element(byte[] type) {
this.type = type;
}
/** Read the element data
*/
public void readData(DataSource source) {
//Setup a buffer for it's data
this.data = new byte[(int)size];
//Read the data
source.read(this.data, 0, this.data.length);
dataRead = true;
}
/** Skip the element data
*/
public void skipData(DataSource source) {
if (!dataRead) {
// Skip the data
source.skip(size);
dataRead = true;
}
}
public long writeElement(DataWriter writer)
{
return writeHeaderData(writer) + writeData(writer);
}
/** Write the element header data.
* Override this in sub-classes for more specialized writing.
*/
public long writeHeaderData(DataWriter writer)
{
long len = 0;
byte [] type = getType();
len += type.length;
writer.write(type);
byte [] size = Element.makeEbmlCodedSize(getSize());
len += size.length;
writer.write(size);
return len;
}
/** Write the element data.
* Override this in sub-classes for more specialized writing.
*/
public long writeData(DataWriter writer)
{
return writer.write(this.data, 0, this.data.length);
}
/** Getter for property data.
* @return Value of property data.
*
*/
public byte[] getData() {
return this.data;
}
/** Setter for property data.
* @param data New value of property data.
*
*/
public void setData(byte[] data) {
this.data = data;
this.size = data.length;
}
/** Clears the data of this element, useful if you just want
* this element to be a placeholder
*/
public void clearData()
{
this.data = null;
}
/** Getter for property size.
* @return Value of property size.
*
*/
public long getSize() {
return size;
}
/** Setter for property size.
* @param size New value of property size.
*
*/
public void setSize(long size) {
this.size = size;
}
/** Get the total size of this element
*/
public long getTotalSize()
{
long totalSize = 0;
totalSize += getType().length;
//totalSize += Element.codedSizeLength(getSize());
totalSize += this.headerSize;
totalSize += getSize();
return totalSize;
}
/** Getter for property type.
* @return Value of property type.
*
*/
public byte[] getType() {
return type;
}
/** Setter for property type.
* @param type New value of property type.
*
*/
public void setType(byte[] type) {
this.type = type;
}
public void setElementType(ElementType typeInfo) {
this.typeInfo = typeInfo;
}
public ElementType getElementType() {
return typeInfo;
}
/** Getter for property parent.
* @return Value of property parent.
*
*/
public Element getParent() {
return this.parent;
}
/** Setter for property parent.
* @param parent New value of property parent.
*
*/
public void setParent(Element parent) {
this.parent = parent;
}
public byte[] toByteArray() {
byte[] head = makeEbmlCode(type, size);
byte[] ret = new byte[head.length + data.length];
org.ebml.util.ArrayCopy.arraycopy(head, 0, ret, 0, head.length);
org.ebml.util.ArrayCopy.arraycopy(data, 0, ret, head.length, data.length);
return ret;
}
public boolean equals(byte [] typeId) {
return ElementType.compareIDs(this.type, typeId);
}
public boolean equals(ElementType elemType) {
return this.equals(elemType.id);
}
public static void setMinSizeLength(int minSize) {
MIN_SIZE_LENGTH = minSize;
}
public static int getMinSizeLength() {
return MIN_SIZE_LENGTH;
}
public static byte[] makeEbmlCode(byte[] typeID, long size) {
int codedLen = codedSizeLength(size);
byte[] ret = new byte[typeID.length + codedLen];
ArrayCopy.arraycopy(typeID, 0, ret, 0, typeID.length);
byte[] codedSize = makeEbmlCodedSize(size);
ArrayCopy.arraycopy(codedSize, 0, ret, typeID.length, codedSize.length);
return ret;
}
public static byte[] makeEbmlCodedSize(long size) {
int len = codedSizeLength(size);
byte[] ret = new byte[len];
//byte[] packedSize = packIntUnsigned(size);
long mask = 0x00000000000000FFL;
for (int i = 0; i < len; i++) {
ret[len - 1 - i] = (byte)((size & mask) >>> (i * 8));
mask <<= 8;
}
//The first size bits should be clear, otherwise we have an error in the size determination.
ret[0] |= 0x80 >> (len - 1);
return ret;
}
public static int getMinByteSize(long value) {
if (value <= 0x7F && value >= 0x80) {
return 1;
}
else if (value <= 0x7FFF && value >= 0x8000) {
return 2;
}
else if (value <= 0x7FFFFF && value >= 0x800000) {
return 3;
}
else if (value <= 0x7FFFFFFF && value >= 0x80000000) {
return 4;
}
else if (value <= 0x7FFFFFFFFFL && value >= 0x8000000000L) {
return 5;
}
else if (value <= 0x7FFFFFFFFFFFL && value >= 0x800000000000L) {
return 6;
}
else if (value <= 0x7FFFFFFFFFFFFFL && value >= 0x80000000000000L) {
return 7;
}
else {
return 8;
}
}
public static int getMinByteSizeUnsigned(long value) {
int size = 8;
long mask = 0xFF00000000000000L;
for (int i = 0; i < 8; i++) {
if ((value & mask) == 0) {
mask = mask >>> 8;
size--;
}
else {
return size;
}
}
return 8;
}
public static int codedSizeLength(long value) {
int codedSize = 0;
if (value < 127) {
codedSize = 1;
}
else if (value < 16383) {
codedSize = 2;
}
else if (value < 2097151) {
codedSize = 3;
}
else if (value < 268435455) {
codedSize = 4;
}
if ((MIN_SIZE_LENGTH > 0) && (codedSize <= MIN_SIZE_LENGTH)) {
codedSize = MIN_SIZE_LENGTH;
}
else {
//codedSize = 8;
}
return codedSize;
}
public static byte[] packIntUnsigned(long value) {
int size = getMinByteSizeUnsigned(value);
return packInt(value, size);
}
public static byte[] packInt(long value) {
int size = getMinByteSize(value);
return packInt(value, size);
}
public static byte[] packInt(long value, int size)
{
byte[] ret = new byte[size];
long mask = 0x00000000000000FFL;
int b = size - 1;
for (int i = 0; i < size; i++)
{
ret[b] = (byte)(((value >>> (8 * i)) & mask));
b--;
}
return ret;
}
public void setHeaderSize(int headerSize) {
this.headerSize = headerSize;
}
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml;
import java.util.*;
public class ElementType {
public static short UNKNOWN_ELEMENT = 0;
public static short MASTER_ELEMENT = 1;
public static short BINARY_ELEMENT = 2;
public static short SINTEGER_ELEMENT = 3;
public static short UINTEGER_ELEMENT = 4;
public static short FLOAT_ELEMENT = 5;
public static short STRING_ELEMENT = 6;
public static short ASCII_STRING_ELEMENT = 7;
public static short DATE_ELEMENT = 8;
public static short LAST_ELEMENT_TYPE = 100;
public String name;
public short level;
public byte [] id;
public short type;
//public HashMap child;
public ArrayList<ElementType> children;
public ElementType() {
}
public ElementType(String name, short level, byte [] id, short type, ArrayList<ElementType> children) {
this.name = name;
this.level = level;
this.id = id;
this.type = type;
this.children = children;
}
public ElementType findElement(byte [] id) {
if (this.isElement(id))
return this;
if (children != null) {
for (int i = 0; i < children.size(); i++) {
ElementType entry = (ElementType)children.get(i);
if (entry.isElement(id))
return entry;
entry = entry.findElement(id);
if (entry != null)
return entry;
}
}
return null;
}
public boolean isElement(byte [] id) {
return ElementType.compareIDs(this.id, id);
}
public static boolean compareIDs(byte[] id1, byte[] id2) {
if ((id1 == null)
|| (id2 == null)
|| (id1.length != id2.length))
return false;
for (int i = 0; i < id1.length; i++) {
if (id1[i] != id2[i])
return false;
}
return true;
}
public Element createElement() {
Element elem;
if (this.type == ElementType.MASTER_ELEMENT) {
elem = new MasterElement(this.id);
} else if (this.type == ElementType.BINARY_ELEMENT) {
elem = new BinaryElement(this.id);
} else if (this.type == ElementType.STRING_ELEMENT) {
elem = new StringElement(this.id);
} else if (this.type == ElementType.ASCII_STRING_ELEMENT) {
elem = new StringElement(this.id, "US-ASCII");
} else if (this.type == ElementType.SINTEGER_ELEMENT) {
elem = new SignedIntegerElement(this.id);
} else if (this.type == ElementType.UINTEGER_ELEMENT) {
elem = new UnsignedIntegerElement(this.id);
} else if (this.type == ElementType.FLOAT_ELEMENT) {
elem = new FloatElement(this.id);
} else if (this.type == ElementType.DATE_ELEMENT) {
elem = new DateElement(this.id);
} else if (this.type == ElementType.UNKNOWN_ELEMENT) {
elem = new BinaryElement(this.id);
} else {
return null;
}
elem.setElementType(this);
return elem;
}
}
\ No newline at end of file
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml;
import java.io.*;
public class FloatElement extends BinaryElement {
public FloatElement(byte[] type) {
super(type);
}
/**
* Set the float value of this element
* @param value Float value to set
* @throws ArithmeticException if the float value is larger than Double.MAX_VALUE
*/
public void setValue(double value) {
try {
if (value < Float.MAX_VALUE) {
ByteArrayOutputStream bIO = new ByteArrayOutputStream(4);
DataOutputStream dIO = new DataOutputStream(bIO);
dIO.writeFloat((float)value);
setData(bIO.toByteArray());
} else if (value < Double.MAX_VALUE) {
ByteArrayOutputStream bIO = new ByteArrayOutputStream(8);
DataOutputStream dIO = new DataOutputStream(bIO);
dIO.writeDouble(value);
setData(bIO.toByteArray());
} else {
throw new ArithmeticException(
"80-bit floats are not supported, BTW How did you create such a large float in Java?");
}
} catch (IOException ex) {
return;
}
}
/**
* Get the float value of this element
* @return Float value of this element
* @throws ArithmeticException for 80-bit or 10-byte floats. AFAIK Java doesn't support them
*/
public double getValue() {
try {
if (size == 4) {
float value = 0;
ByteArrayInputStream bIS = new ByteArrayInputStream(data);
DataInputStream dIS = new DataInputStream(bIS);
value = dIS.readFloat();
return value;
} else if (size == 8) {
double value = 0;
ByteArrayInputStream bIS = new ByteArrayInputStream(data);
DataInputStream dIS = new DataInputStream(bIS);
value = dIS.readDouble();
return value;
} else {
throw new ArithmeticException(
"80-bit floats are not supported");
}
} catch (IOException ex) {
return 0;
}
}
}
\ No newline at end of file
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml;
import org.ebml.io.*;
import java.util.*;
public class MasterElement extends Element {
protected long usedSize;
protected ArrayList<Element> children = new ArrayList<Element>();
public MasterElement(byte[] type) {
super(type);
usedSize = 0;
}
public Element readNextChild(EBMLReader reader) {
if (usedSize >= this.getSize())
return null;
Element elem = reader.readNextElement();
if (elem == null)
return null;
elem.setParent(this);
usedSize += elem.getTotalSize();
return elem;
}
/* Skip the element data */
public void skipData(DataSource source) {
// Skip the child elements
source.skip(size-usedSize);
}
public long writeData(DataWriter writer)
{
long len = 0;
for (int i = 0; i < children.size(); i++)
{
Element elem = (Element)children.get(i);
len += elem.writeElement(writer);
}
return len;
}
public void addChildElement(Element elem)
{
children.add(elem);
size += elem.getTotalSize();
}
}
\ No newline at end of file
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml;
/*
* SignedInteger.java
*
* Created on February 17, 2003, 1:54 PM
*/
/**
* Basic class for the Signed Integer EBML data type.
* @author John Cannon
*/
public class SignedIntegerElement
extends BinaryElement {
public SignedIntegerElement(byte[] typeID) {
super(typeID);
}
public void setValue(long value) {
//System.out.println(Long.toHexString(value));
setData(packInt(value));
/*for (int i = 0; i < data.length; i++) {
System.out.print(Integer.toHexString(data[i]) + ", ");
}
System.out.print("\n");*/
}
public long getValue() {
long l = 0;
long tmp = 0;
l |= ((long)data[0] << (56 - ((8 - data.length) * 8)));
for (int i = 1; i < data.length; i++) {
tmp = ((long)data[data.length - i]) << 56;
tmp >>>= 56 - (8 * (i - 1));
l |= tmp;
}
//System.out.println(Long.toHexString(l));
return l;
}
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml;
/**
* Baisc class for handling an EBML string data type. This class encapsulates
* both UTF and ASCII string types and can use any string type supported by
* the Java platform.
*
* @author John Cannon
*/
public class StringElement
extends org.ebml.BinaryElement {
private String charset = "UTF-8";
/** Creates a new instance of StringElement */
public StringElement(byte[] typeID) {
super(typeID);
}
public StringElement(byte[] typeID, String encoding) {
super(typeID);
charset = encoding;
}
private boolean checkForCharsetHack()
{
// Check if we are trying to read UTF-8, if so lets try UTF8.
// Microsofts Java supports "UTF8" but not "UTF-8"
if (charset.compareTo("UTF-8") == 0)
{
charset = "UTF8";
// Let's try again
return true;
}
else if (charset.compareTo("US-ASCII") == 0)
{
// This is the same story as UTF-8,
// If Microsoft is going to hijack Java they should at least support the orignal :>
charset = "ASCII";
// Let's try again
return true;
}
return false;
}
public String getValue() {
try {
if (data == null)
throw new java.lang.IllegalStateException("Call readData() before trying to extract the string value.");
return new String(data, charset);
}
catch (java.io.UnsupportedEncodingException ex) {
if (checkForCharsetHack())
{
return getValue();
}
ex.printStackTrace();
return "";
}
}
public void setValue(String value) {
try {
setData(value.getBytes(charset));
}
catch (java.io.UnsupportedEncodingException ex) {
if (checkForCharsetHack())
{
setValue(value);
return;
}
ex.printStackTrace();
}
}
public String getEncoding() {
return charset;
}
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml;
import java.util.*;
public class UnknownElementType extends ElementType {
public UnknownElementType(byte [] id) {
super("Unknown", (short)0, id, ElementType.UNKNOWN_ELEMENT, (ArrayList<ElementType>)null);
}
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml;
/*
* UnsignedIntegerElement.java
*
* Created on February 15, 2003, 6:27 PM
*/
/**
* Basic class for the Unsigned Integer data type in EBML.
* @author John Cannon
*/
public class UnsignedIntegerElement
extends org.ebml.BinaryElement {
public UnsignedIntegerElement(byte[] typeID) {
super(typeID);
}
public void setValue(long value) {
setData(packIntUnsigned(value));
}
public long getValue() {
long l = 0;
long tmp = 0;
for (int i = 0; i < data.length; i++) {
tmp = ((long)data[data.length - 1 - i]) << 56;
tmp >>>= (56 - (i * 8));
l |= tmp;
}
return l;
}
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml.io;
/**
* Interface for seeking operations.
* Not designed to be used alone but as a super interface for a more
* specialied reading or writing interfaces.
*/
public interface DataSeekable
{
/**
* Returns the length
*
* @return <code>-1</code> if the length is unknown
* @return <code> >0</code> length of the <code>DataSeekable</code>
*/
public long length();
public long getFilePointer();
/**
* Check if the <code>DataSeekable</code> object is seekable
*
* @return <code>true</code> if seeking is supported.
* @return <code>false</code> if seeking is not supported.
*/
public boolean isSeekable();
/**
* Seeks in the <code>DataSeekable</code>
*
* @param pos Absolute position to seek to
* @return The new file position
*/
public long seek(long pos);
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml.io;
/*
* DataSource.java
*
* Created on November 18, 2002, 4:08 PM
*/
/**
* Defines the interface used for custom <code>DataSource</code>'s. A
* <code>DataSource</code> provides methods of reading bytes individually or
* in arrays. These basic functions must be defined in any <code>DataSource</code>
* objects to be used with the <code>EBMLReader</code>.
*
* @author John Cannon
* @author Jory Stone
*/
public interface DataSource extends DataSeekable {
public byte readByte();
public int read(byte[] buff);
public int read(byte[] buff, int offset, int length);
/**
* Skip a number of bytes in the <code>DataSeekable</code>
*
* @param offset The number of bytes to skip
* @return The number of bytes skipped
*/
public long skip(long offset);
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml.io;
/**
* Defines the interface used for custom <code>DataWriter</code>'s. A
* <code>DataWriter</code> provides methods of writing bytes individually or
* in arrays. These basic functions must be defined in any <code>DataWriter</code>
* objects to be used with the <code>EBMLWriter</code>.
*
* @author Jory Stone
*/
public interface DataWriter extends DataSeekable
{
public int write(byte b);
public int write(byte[] buff);
public int write(byte[] buff, int offset, int length);
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml.io;
import java.io.*;
public class FileDataSource implements DataSource {
RandomAccessFile file = null;
public FileDataSource(String filename) throws FileNotFoundException, IOException {
file = new RandomAccessFile(filename, "r");
}
public FileDataSource(String filename, String mode) throws FileNotFoundException, IOException {
file = new RandomAccessFile(filename, mode);
}
public byte readByte() {
try {
return file.readByte();
} catch (IOException ex) {
return 0;
}
}
public int read(byte[] buff) {
try {
return file.read(buff);
} catch (IOException ex) {
return 0;
}
}
public int read(byte[] buff, int offset, int length) {
try {
return file.read(buff, offset, length);
} catch (IOException ex) {
return 0;
}
}
public long skip(long offset) {
try {
return file.skipBytes((int)offset);
} catch (IOException ex) {
return 0;
}
}
public long length() {
try {
return file.length();
} catch (IOException ex) {
return -1;
}
}
public long getFilePointer() {
try {
return file.getFilePointer();
} catch (IOException ex) {
return -1;
}
}
public boolean isSeekable() {
return true;
}
public long seek(long pos) {
try {
file.seek(pos);
return file.getFilePointer();
} catch (IOException ex) {
return -1;
}
}
}
\ No newline at end of file
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml.io;
import java.io.*;
public class FileDataWriter implements DataWriter
{
RandomAccessFile file = null;
public FileDataWriter(String filename) throws FileNotFoundException, IOException
{
file = new RandomAccessFile(filename, "rw");
}
public FileDataWriter(String filename, String mode) throws FileNotFoundException, IOException
{
file = new RandomAccessFile(filename, mode);
}
public int write(byte b)
{
try
{
file.write(b);
return 1;
}
catch (IOException ex)
{
return 0;
}
}
public int write(byte[] buff)
{
try
{
file.write(buff);
return buff.length;
}
catch (IOException ex)
{
return 0;
}
}
public int write(byte[] buff, int offset, int length)
{
try
{
file.write(buff, offset, length);
return length;
}
catch (IOException ex)
{
return 0;
}
}
public long length()
{
try
{
return file.length();
}
catch (IOException ex)
{
return -1;
}
}
public long getFilePointer()
{
try
{
return file.getFilePointer();
}
catch (IOException ex)
{
return -1;
}
}
public boolean isSeekable()
{
return true;
}
public void close()
{
try
{
file.close();
}
catch (IOException ex)
{
}
}
public long seek(long pos)
{
try
{
file.seek(pos);
return file.getFilePointer();
}
catch (IOException ex)
{
return -1;
}
}
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml.io;
import java.io.*;
/*
* InputStreamDataSource
*
* Created on November 19, 2002, 9:35 PM
*
* @author John Cannon
*/
public class InputStreamDataSource
implements DataSource {
protected InputStream in = null;
protected long pos = 0;
protected byte[] buffer = new byte[1];
public InputStream getInputStream() {
return in;
}
/** Creates a new instance of InputStreamDataSource */
public InputStreamDataSource(InputStream in) {
this.in = in;
}
public byte readByte() {
try {
int l = in.read(buffer);
pos += l;
return buffer[0];
}
catch (IOException e) {
e.printStackTrace();
return -1;
}
}
public int read(byte[] buff) {
try {
int l = in.read(buff);
pos += l;
return l;
}
catch (IOException e) {
e.printStackTrace();
return -1;
}
}
public int read(byte[] buff, int offset, int length) {
try {
int l = in.read(buff, offset, length);
pos += l;
return l;
}
catch (IOException e) {
e.printStackTrace();
return -1;
}
}
public long skip(long offset) {
try {
long l = in.skip(offset);
pos += l;
return l;
}
catch (IOException e) {
e.printStackTrace();
return -1;
}
}
public long length() {
try {
return pos + in.available();
}
catch (IOException e) {
e.printStackTrace();
return -1;
}
}
public long getFilePointer() {
return pos;
}
public boolean isSeekable() {
return false;
}
public long seek(long pos) {
return pos;
}
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml.io;
import java.io.*;
public class StreamDataWriter implements DataWriter
{
OutputStream stream = null;
public StreamDataWriter(OutputStream stream) throws IOException
{
this.stream = stream;
}
public int write(byte b)
{
try
{
stream.write(b);
stream.flush();
return 1;
}
catch (IOException ex)
{
return 0;
}
}
public int write(byte[] buff)
{
try
{
stream.write(buff);
stream.flush();
return buff.length;
}
catch (IOException ex)
{
return 0;
}
}
public int write(byte[] buff, int offset, int length)
{
try
{
stream.write(buff, offset, length);
stream.flush();
return length;
}
catch (IOException ex)
{
return 0;
}
}
public boolean isSeekable()
{
return false;
}
public long seek(long pos)
{
return -1;
}
public long getFilePointer()
{
return -1;
}
public long length()
{
return -1;
}
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml.matroska;
import org.ebml.*;
import org.ebml.util.*;
public class MatroskaBlock extends BinaryElement {
protected int [] Sizes = null;
protected int HeaderSize = 0;
protected int BlockTimecode = 0;
protected int TrackNo = 0;
private boolean keyFrame;
public MatroskaBlock(byte[] type) {
super(type);
}
//public void readData(DataSource source) {
// parseBlock();
//}
public void parseBlock() {
int index = 0;
TrackNo = (int)EBMLReader.readEBMLCode(data);
index = Element.codedSizeLength(TrackNo);
HeaderSize += index;
short BlockTimecode1 = (short)(data[index++] & 0xFF);
short BlockTimecode2 = (short)(data[index++] & 0xFF);
if (BlockTimecode1 != 0 || BlockTimecode2 != 0) {
BlockTimecode = (BlockTimecode1 << 8) | BlockTimecode2;
}
int keyFlag = data[index] & 0x80;
if(keyFlag > 0)
this.keyFrame = true;
else
this.keyFrame = false;
int LaceFlag = data[index] & 0x06;
index++;
// Increase the HeaderSize by the number of bytes we have read
HeaderSize += 3;
if (LaceFlag != 0x00) {
// We have lacing
byte LaceCount = data[index++];
HeaderSize += 1;
if (LaceFlag == 0x02) { // Xiph Lacing
Sizes = readXiphLaceSizes(index, LaceCount);
} else if (LaceFlag == 0x06) { // EBML Lacing
Sizes = readEBMLLaceSizes(index, LaceCount);
} else if (LaceFlag == 0x04) { // Fixed Size Lacing
Sizes = new int[LaceCount+1];
Sizes[0] = (int)(this.getSize() - HeaderSize) / (LaceCount+1);
for (int s = 0; s < LaceCount; s++)
Sizes[s+1] = Sizes[0];
} else {
throw new RuntimeException("Unsupported lacing type flag.");
}
}
//data = new byte[(int)(this.getSize() - HeaderSize)];
//source.read(data, 0, data.length);
//this.dataRead = true;
}
public int[] readEBMLLaceSizes(int index, short LaceCount) {
int [] LaceSizes = new int[LaceCount+1];
LaceSizes[LaceCount] = (int)this.getSize();
// This uses the DataSource.getBytePosition() for finding the header size
// because of the trouble of finding the byte size of sized ebml coded integers
//long ByteStartPos = source.getFilePointer();
int startIndex = index;
LaceSizes[0] = (int)EBMLReader.readEBMLCode(data, index);
index += Element.codedSizeLength(LaceSizes[0]);
LaceSizes[LaceCount] -= LaceSizes[0];
long FirstEBMLSize = LaceSizes[0];
long LastEBMLSize = 0;
for (int l = 0; l < LaceCount-1; l++) {
LastEBMLSize = EBMLReader.readSignedEBMLCode(data, index);
index += Element.codedSizeLength(LastEBMLSize);
FirstEBMLSize += LastEBMLSize;
LaceSizes[l+1] = (int)FirstEBMLSize;
// Update the size of the last block
LaceSizes[LaceCount] -= LaceSizes[l+1];
}
//long ByteEndPos = source.getFilePointer();
//HeaderSize = HeaderSize + (int)(ByteEndPos - ByteStartPos);
HeaderSize = HeaderSize + (int)(index - startIndex);
LaceSizes[LaceCount] -= HeaderSize;
return LaceSizes;
}
public int[] readXiphLaceSizes(int index, short LaceCount) {
int [] LaceSizes = new int[LaceCount+1];
LaceSizes[LaceCount] = (int)this.getSize();
//long ByteStartPos = source.getFilePointer();
for (int l = 0; l < LaceCount; l++) {
short LaceSizeByte = 255;
while (LaceSizeByte == 255) {
LaceSizeByte = (short)(data[index++] & 0xFF);
HeaderSize += 1;
LaceSizes[l] += LaceSizeByte;
}
// Update the size of the last block
LaceSizes[LaceCount] -= LaceSizes[l];
}
//long ByteEndPos = source.getFilePointer();
LaceSizes[LaceCount] -= HeaderSize;
return LaceSizes;
}
public int getFrameCount() {
if (Sizes == null) {
return 1;
}
return Sizes.length;
}
public byte [] getFrame(int frame) {
if (Sizes == null) {
if (frame != 0) {
throw new IllegalArgumentException("Tried to read laced frame on non-laced Block. MatroskaBlock.getFrame(frame > 0)");
}
byte [] FrameData = new byte[data.length-HeaderSize];
ArrayCopy.arraycopy(data, HeaderSize, FrameData, 0, FrameData.length);
return FrameData;
}
byte [] FrameData = new byte[Sizes[frame]];
// Calc the frame data offset
int StartOffset = HeaderSize;
for (int s = 0; s < frame; s++) {
StartOffset += Sizes[s];
}
// Copy the frame data
ArrayCopy.arraycopy(data, StartOffset, FrameData, 0, FrameData.length);
return FrameData;
}
public long getAdjustedBlockTimecode(long ClusterTimecode, long TimecodeScale) {
return ClusterTimecode + (BlockTimecode);// * TimecodeScale);
}
public int getTrackNo() {
return TrackNo;
}
public int getBlockTimecode() {
return BlockTimecode;
}
public void setFrameData(int trackNo, long timecode, byte [] frameData)
{
this.data = new byte[4 + frameData.length];
this.data[0] = (byte) (trackNo | 0x80);
this.data[1] = (byte) (timecode >> 8);
this.data[2] = (byte) (timecode & 0xff);
this.data[3] = 0; // flags
ArrayCopy.arraycopy(frameData, 0, this.data, 4, frameData.length);
setSize(4 + frameData.length);
}
public boolean isKeyFrame() {
return keyFrame;
}
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml.matroska;
import org.ebml.*;
import org.ebml.util.*;
/**
* Summary description for MatroskaCluster.
*/
public class MatroskaCluster extends MasterElement
{
static public int NO_LACING = 0;
static public int XIPH_LACING = 1;
static public int EBML_LACING = 2;
protected int [] laceMode = null;
protected TLinkedList frames = new TLinkedList();
protected long clusterTimecode = 0;
public MatroskaCluster(byte[] type)
{
super(type);
}
/**
* Set the current lacing mode.
*
* @param trackNo Track Number for the track to enable lacing for. 1-based index
* @param laceMode The lacing moe to use. See NO_LACING, XIPH_LACING, and EBML_LACING.
*/
void setLaceMode(short trackNo, int laceMode)
{
if (this.laceMode == null)
{
this.laceMode = new int[trackNo];
}
if (this.laceMode.length < trackNo)
{
int [] oldLaceMode = this.laceMode;
this.laceMode = new int[trackNo];;
ArrayCopy.arraycopy(this.laceMode, 0, oldLaceMode, 0, oldLaceMode.length);
}
this.laceMode[trackNo-1] = laceMode;
}
/**
* Get the current lacing mode.
*
* @param trackNo Track Number for the track to enable lacing for. 1-based index
* @return -1 if the track no is invalid
*/
int getLaceMode(short trackNo)
{
if (this.laceMode == null)
{
return -1;
}
if (this.laceMode.length < trackNo)
{
return -1;
}
return this.laceMode[trackNo-1];
}
public void AddFrame(MatroskaFileFrame frame)
{
// Is this the earliest timecode?
if (frame.Timecode < clusterTimecode)
{
clusterTimecode = frame.Timecode;
}
frames.add(frame);
}
public void FlushFrames()
{
TLinkedList.IteratorImpl iter = frames.first();
while (iter.hasNext())
{
//MatroskaFileFrame frame = (MatroskaFileFrame)
iter.next();
}
}
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml.matroska;
import org.ebml.*;
import java.util.*;
/**
* DocType for Matroska files.
* This has all the element type id declares and also is in charge of
* creating the correct element classes from the type id.
*/
public class MatroskaDocType implements DocType
{
// Custom Element Types
static public short BLOCK_ELEMENT = (short)(ElementType.LAST_ELEMENT_TYPE + 1);
static public short SEGMENT_ELEMENT = (short)(ElementType.LAST_ELEMENT_TYPE + 2);
static public short CLUSTER_ELEMENT = (short)(ElementType.LAST_ELEMENT_TYPE + 3);
// EBML Id's
static public byte [] Void_Id = {(byte)0xEC};
static public byte [] EBMLHeader_Id = {0x1A, 0x45, (byte)0xDF, (byte)0xA3};
static public byte [] EBMLVersion_Id = {0x42, (byte)0x86};
static public byte [] DocTypeReadVersion_Id = {0x42, (byte)0x85};
static public byte [] EBMLReadVersion_Id = {0x42, (byte)0xF7};
static public byte [] EBMLMaxIDLength_Id = {0x42, (byte)0xF2};
static public byte [] EBMLMaxSizeLength_Id = {0x42, (byte)0xF3};
static public byte [] DocType_Id = {0x42, (byte)0x82};
static public byte [] DocTypeVersion_Id = {0x42, (byte)0x87};
static public byte [] Segment_Id = {0x18, 0x53, (byte)0x80, 0x67};
static public byte [] SeekHead_Id = {0x11, 0x4D, (byte)0x9B, 0x74};
static public byte [] SeekEntry_Id = {0x4D, (byte)0xBB};
static public byte [] SeekID_Id = {0x53, (byte)0xAB};
static public byte [] SeekPosition_Id = {0x53, (byte)0xAC};
static public byte [] SegmentInfo_Id = {0x15, (byte)0x49, (byte)0xA9, (byte)0x66};
static public byte [] SegmentUID_Id = {0x73, (byte)0xA4};
static public byte [] SegmentFilename_Id = {0x73, (byte)0x84};
static public byte [] TimecodeScale_Id = {0x2A, (byte)0xD7, (byte)0xB1};
static public byte [] Duration_Id = {0x44, (byte)0x89};
static public byte [] DateUTC_Id = {0x44, (byte)0x61};
static public byte [] Title_Id = {0x7B, (byte)0xA9};
static public byte [] MuxingApp_Id = {0x4D, (byte)0x80};
static public byte [] WritingApp_Id = {0x57, 0x41};
static public byte [] Tracks_Id = {0x16, (byte)0x54, (byte)0xAE, (byte)0x6B};
static public byte [] TrackEntry_Id = {(byte)0xAE};
static public byte [] TrackNumber_Id = {(byte)0xD7};
static public byte [] TrackUID_Id = {0x73, (byte)0xC5};
static public byte [] TrackType_Id = {(byte)0x83};
static public byte [] TrackDefaultDuration_Id = {0x23, (byte)0xE3, (byte)0x83};
static public byte [] TrackName_Id = {0x53, 0x6E};
static public byte [] TrackLanguage_Id = {0x22, (byte)0xB5, (byte)0x9C};
static public byte [] TrackCodecID_Id = {(byte)0x86};
static public byte [] TrackCodecPrivate_Id = {(byte)0x63, (byte)0xA2};
static public byte [] TrackVideo_Id = {(byte)0xE0};
static public byte [] PixelWidth_Id = {(byte)0xB0};
static public byte [] PixelHeight_Id = {(byte)0xBA};
static public byte [] DisplayWidth_Id = {0x54, (byte)0xB0};
static public byte [] DisplayHeight_Id = {0x54, (byte)0xBA};
static public byte [] TrackAudio_Id = {(byte)0xE1};
static public byte [] SamplingFrequency_Id = {(byte)0xB5};
static public byte [] OutputSamplingFrequency_Id = {0x78, (byte)0xB5};
static public byte [] Channels_Id = {(byte)0x9F};
static public byte [] BitDepth_Id = {0x62, 0x64};
static public byte [] Attachments_Id = {0x19, 0x41, (byte)0xA4, 0x69};
static public byte [] AttachedFile_Id = {0x61, (byte)0xA7};
static public byte [] AttachedFileDescription_Id = {0x46, (byte)0x7E};
static public byte [] AttachedFileName_Id = {0x46, (byte)0x6E};
static public byte [] AttachedFileMimeType_Id = {0x46, (byte)0x60};
static public byte [] AttachedFileData_Id = {0x46, (byte)0x5C};
static public byte [] AttachedFileUID_Id = {0x46, (byte)0xAE};
static public byte [] Tags_Id = {0x12, (byte)0x54, (byte)0xC3, (byte)0x67};
static public byte [] Tag_Id = {0x73, (byte)0x73};
static public byte [] TagTargets_Id = {0x63, (byte)0xC0};
static public byte [] TagTargetTrackUID_Id = {0x63, (byte)0xC5};
static public byte [] TagTargetChapterUID_Id = {0x63, (byte)0xC4};
static public byte [] TagTargetAttachmentUID_Id = {0x63, (byte)0xC6};
static public byte [] TagSimpleTag_Id = {0x67, (byte)0xC8};
static public byte [] TagSimpleTagName_Id = {0x45, (byte)0xA3};
static public byte [] TagSimpleTagString_Id = {0x44, (byte)0x87};
static public byte [] TagSimpleTagBinary_Id = {0x44, (byte)0x85};
static public byte [] Cluster_Id = {0x1F, (byte)0x43, (byte)0xB6, (byte)0x75};
static public byte[] ClusterTimecode_Id = {(byte)0xE7};
static public byte[] ClusterBlockGroup_Id = {(byte)0xA0};
static public byte[] ClusterBlock_Id = {(byte)0xA1};
static public byte[] ClusterSimpleBlock_Id = {(byte)0xA3};
static public byte[] ClusterBlockDuration_Id = {(byte)0x9B};
static public byte[] ClusterReferenceBlock_Id = {(byte)0xFB};
static public byte[] Chapters_Id = {0x10, (byte)0x43, (byte)0xA7, (byte)0x70};
static public byte [] ChapterEditionEntry_Id = {(byte)0x45, (byte)0xB9};
static public byte [] ChapterEditionUID_Id = {(byte)0x45, (byte)0xBC};
static public byte [] ChapterEditionFlagHidden_Id = {(byte)0x45, (byte)0xBD};
static public byte [] ChapterEditionFlagDefault_Id = {(byte)0x45, (byte)0xDB};
static public byte [] ChapterEditionManaged_Id = {(byte)0x45, (byte)0xDD};
static public byte [] ChapterAtom_Id = {(byte)0xB6};
static public byte [] ChapterAtomChapterUID_Id = {(byte)0x73, (byte)0xC4};
static public byte [] ChapterAtomChapterTimeStart_Id = {(byte)0x91};
static public byte [] ChapterAtomChapterTimeEnd_Id = {(byte)0x92};
static public byte [] ChapterAtomChapterFlagHidden_Id = {(byte)0x98};
static public byte [] ChapterAtomChapterFlagEnabled_Id = {(byte)0x45, (byte)0x98};
static public byte [] ChapterAtomChapterPhysicalEquiv_Id = {(byte)0x63, (byte)0xC3};
static public byte [] ChapterAtomChapterTrack_Id = {(byte)0x8F};
static public byte [] ChapterAtomChapterTrackNumber_Id = {(byte)0x89};
static public byte [] ChapterAtomChapterDisplay_Id = {(byte)0x80};
static public byte [] ChapterAtomChapString_Id = {(byte)0x85};
static public byte [] ChapterAtomChapLanguage_Id = {(byte)0x43, (byte)0x7C};
static public byte [] ChapterAtomChapCountry_Id = {(byte)0x43, (byte)0x7E};
// Track Types
static public byte track_video = 0x01; ///< Rectangle-shaped non-transparent pictures aka video
static public byte track_audio = 0x02; ///< Anything you can hear
static public byte track_complex = 0x03; ///< Audio and video in same track, used by DV
static public byte track_logo = 0x10; ///< Overlay-pictures, displayed over video
static public byte track_subtitle = 0x11; ///< Text-subtitles. One track contains one language and only one track can be active (player-side configuration)
static public byte track_control = 0x20; ///< Control-codes for menus and other stuff
/**
* Converts a integer track type to String form.
*
* @param trackType Integer Track Type
* @return String <code>trackType</code> in String form
*/
static public String TrackTypeToString(byte trackType) {
if (trackType == track_video)
return "Video";
if (trackType == track_audio)
return "Audio";
if (trackType == track_complex)
return "Complex";
if (trackType == track_logo)
return "Logo";
if (trackType == track_subtitle)
return "Subtitle";
if (trackType == track_control)
return "Control";
return "";
}
protected ElementType type;
static public MatroskaDocType obj = new MatroskaDocType();
public MatroskaDocType() {
//long start = java.lang.System.currentTimeMillis();
init();
//System.out.println("MatroskaDocType Loaded in " + java.lang.System.currentTimeMillis() - start + " milliseconds");
}
protected void init() {
try {
ElementType baseLevel = new ElementType("", (short)0, (byte[])null,
(short)0, new ArrayList<ElementType>());
ElementType level0 = null;
ElementType level1 = null;
ElementType level2 = null;
ElementType level3 = null;
ElementType level4 = null;
ElementType level5 = null;
level0 = new ElementType("Void",
(short)1,
Void_Id,
ElementType.BINARY_ELEMENT,
(ArrayList<ElementType>)null);
baseLevel.children.add(level0);
level0 = new ElementType("EBMLHeader",
(short)0,
EBMLHeader_Id,
ElementType.MASTER_ELEMENT,
new ArrayList<ElementType>());
level1 = new ElementType("EBMLVersion",
(short)1,
EBMLVersion_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level0.children.add(level1);
level1 = new ElementType("EBMLReadVersion",
(short)1,
EBMLReadVersion_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level0.children.add(level1);
level1 = new ElementType("EBMLMaxIDLength",
(short)1,
EBMLMaxIDLength_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level0.children.add(level1);
level1 = new ElementType("EBMLMaxSizeLength",
(short)1,
EBMLMaxSizeLength_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level0.children.add(level1);
level1 = new ElementType("DocType",
(short)1,
DocType_Id,
ElementType.STRING_ELEMENT,
(ArrayList<ElementType>)null);
level0.children.add(level1);
level1 = new ElementType("DocTypeVersion",
(short)1,
DocTypeVersion_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level0.children.add(level1);
level1 = new ElementType("DocTypeReadVersion",
(short)1,
DocTypeReadVersion_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level0.children.add(level1);
baseLevel.children.add(level0);
level0 = new ElementType("Segment",
(short)0,
Segment_Id,
MatroskaDocType.SEGMENT_ELEMENT,
new ArrayList<ElementType>());
level1 = new ElementType("SeekHead",
(short)1,
SeekHead_Id,
ElementType.MASTER_ELEMENT,
new ArrayList<ElementType>());
level2 = new ElementType("SeekEntry",
(short)2,
SeekEntry_Id,
ElementType.MASTER_ELEMENT,
new ArrayList<ElementType>());
level3 = new ElementType("SeekID",
(short)3,
SeekID_Id,
ElementType.BINARY_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
level3 = new ElementType("SeekPosition",
(short)3,
SeekPosition_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
// Add Seek Element
level1.children.add(level2);
// Add SeekHead Element
level0.children.add(level1);
level1 = new ElementType("SegmentInfo",
(short)1,
SegmentInfo_Id,
ElementType.MASTER_ELEMENT,
new ArrayList<ElementType>());
level2 = new ElementType("SegmentUID",
(short)2,
SegmentUID_Id,
ElementType.BINARY_ELEMENT,
(ArrayList<ElementType>)null);
level1.children.add(level2);
level2 = new ElementType("SegmentFilename",
(short)2,
SegmentFilename_Id,
ElementType.STRING_ELEMENT,
(ArrayList<ElementType>)null);
level1.children.add(level2);
level2 = new ElementType("TimecodeScale",
(short)2,
TimecodeScale_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level1.children.add(level2);
level2 = new ElementType("Duration",
(short)2,
Duration_Id,
ElementType.FLOAT_ELEMENT,
(ArrayList<ElementType>)null);
level1.children.add(level2);
level2 = new ElementType("DateUTC",
(short)2,
DateUTC_Id,
ElementType.DATE_ELEMENT,
(ArrayList<ElementType>)null);
level1.children.add(level2);
level2 = new ElementType("Title",
(short)2,
Title_Id,
ElementType.STRING_ELEMENT,
(ArrayList<ElementType>)null);
level1.children.add(level2);
level2 = new ElementType("MuxingApp",
(short)2,
MuxingApp_Id,
ElementType.STRING_ELEMENT,
(ArrayList<ElementType>)null);
level1.children.add(level2);
level2 = new ElementType("WritingApp",
(short)2,
WritingApp_Id,
ElementType.STRING_ELEMENT,
(ArrayList<ElementType>)null);
level1.children.add(level2);
// Add Segment Infomation Element
level0.children.add(level1);
level1 = new ElementType("Tracks",
(short)1,
Tracks_Id,
ElementType.MASTER_ELEMENT,
new ArrayList<ElementType>());
level2 = new ElementType("TrackEntry",
(short)2,
TrackEntry_Id,
ElementType.MASTER_ELEMENT,
new ArrayList<ElementType>());
level3 = new ElementType("TrackNumber",
(short)3,
TrackNumber_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
level3 = new ElementType("TrackUID",
(short)3,
TrackUID_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
level3 = new ElementType("TrackType",
(short)3,
TrackType_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
level3 = new ElementType("TrackDefaultDuration",
(short)3,
TrackDefaultDuration_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
level3 = new ElementType("TrackName",
(short)3,
TrackName_Id,
ElementType.STRING_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
level3 = new ElementType("TrackLanguage",
(short)3,
TrackLanguage_Id,
ElementType.ASCII_STRING_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
level3 = new ElementType("TrackCodecID",
(short)3,
TrackCodecID_Id,
ElementType.ASCII_STRING_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
level3 = new ElementType("TrackCodecPrivate",
(short)3,
TrackCodecPrivate_Id,
ElementType.BINARY_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
level3 = new ElementType("TrackVideo",
(short)3,
TrackVideo_Id,
ElementType.MASTER_ELEMENT,
new ArrayList<ElementType>());
level4 = new ElementType("PixelWidth",
(short)4,
PixelWidth_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level3.children.add(level4);
level4 = new ElementType("PixelHeight",
(short)4,
PixelHeight_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level3.children.add(level4);
level4 = new ElementType("DisplayWidth",
(short)4,
DisplayWidth_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level3.children.add(level4);
level4 = new ElementType("DisplayHeight",
(short)4,
DisplayHeight_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level3.children.add(level4);
// Add TrackVideo Element
level2.children.add(level3);
level3 = new ElementType("TrackAudio",
(short)3,
TrackAudio_Id,
ElementType.MASTER_ELEMENT,
new ArrayList<ElementType>());
level4 = new ElementType("SamplingFrequency",
(short)4,
SamplingFrequency_Id,
ElementType.FLOAT_ELEMENT,
(ArrayList<ElementType>)null);
level3.children.add(level4);
level4 = new ElementType("OutputSamplingFrequency",
(short)4,
OutputSamplingFrequency_Id,
ElementType.FLOAT_ELEMENT,
(ArrayList<ElementType>)null);
level3.children.add(level4);
level4 = new ElementType("Channels",
(short)4,
Channels_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level3.children.add(level4);
level4 = new ElementType("BitDepth",
(short)4,
BitDepth_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level3.children.add(level4);
// Add TrackAudio Element
level2.children.add(level3);
// Add TrackEntry Element
level1.children.add(level2);
// Add Tracks Element
level0.children.add(level1);
level1 = new ElementType("Attachments",
(short)1,
Attachments_Id,
ElementType.MASTER_ELEMENT,
new ArrayList<ElementType>());
level2 = new ElementType("AttachedFile",
(short)2,
AttachedFile_Id,
ElementType.MASTER_ELEMENT,
new ArrayList<ElementType>());
level3 = new ElementType("AttachedFileDescription",
(short)3,
AttachedFileDescription_Id,
ElementType.STRING_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
level3 = new ElementType("AttachedFileName",
(short)3,
AttachedFileName_Id,
ElementType.STRING_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
level3 = new ElementType("AttachedFileMimeType",
(short)3,
AttachedFileMimeType_Id,
ElementType.ASCII_STRING_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
level3 = new ElementType("AttachedFileData",
(short)3,
AttachedFileData_Id,
ElementType.BINARY_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
level3 = new ElementType("AttachedFileUID",
(short)3,
AttachedFileUID_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
// Add AttachedFile Element
level1.children.add(level2);
// Add Attachments Element
level0.children.add(level1);
level1 = new ElementType("Tags",
(short)1,
Tags_Id,
ElementType.MASTER_ELEMENT,
new ArrayList<ElementType>());
level2 = new ElementType("Tag",
(short)2,
Tag_Id,
ElementType.MASTER_ELEMENT,
new ArrayList<ElementType>());
level3 = new ElementType("TagTargets",
(short)3,
TagTargets_Id,
ElementType.MASTER_ELEMENT,
new ArrayList<ElementType>());
level4 = new ElementType("TagTargetTrackUID",
(short)4,
TagTargetTrackUID_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level3.children.add(level4);
level4 = new ElementType("TagTargetChapterUID",
(short)4,
TagTargetChapterUID_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level3.children.add(level4);
level4 = new ElementType("TagTargetAttachmentUID",
(short)4,
TagTargetAttachmentUID_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level3.children.add(level4);
// Add Targets
level2.children.add(level3);
level3 = new ElementType("TagSimpleTag",
(short)3,
TagSimpleTag_Id,
ElementType.MASTER_ELEMENT,
new ArrayList<ElementType>());
level4 = new ElementType("TagSimpleTagName",
(short)4,
TagSimpleTagName_Id,
ElementType.STRING_ELEMENT,
(ArrayList<ElementType>)null);
level3.children.add(level4);
level4 = new ElementType("TagSimpleTagString",
(short)4,
TagSimpleTagString_Id,
ElementType.STRING_ELEMENT,
(ArrayList<ElementType>)null);
level3.children.add(level4);
level4 = new ElementType("TagSimpleTagBinary",
(short)4,
TagSimpleTagBinary_Id,
ElementType.BINARY_ELEMENT,
(ArrayList<ElementType>)null);
level3.children.add(level4);
// Add SimpleTag
level2.children.add(level3);
// Add Tag Element
level1.children.add(level2);
// Add Tags Element
level0.children.add(level1);
level1 = new ElementType("Cluster",
(short)1,
Cluster_Id,
MatroskaDocType.CLUSTER_ELEMENT,
new ArrayList<ElementType>());
level2 = new ElementType("ClusterTimecode",
(short)2,
ClusterTimecode_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level1.children.add(level2);
level2 = new ElementType("ClusterBlockGroup",
(short)2,
ClusterBlockGroup_Id,
ElementType.MASTER_ELEMENT,
new ArrayList<ElementType>());
level3 = new ElementType("ClusterBlock",
(short)3,
ClusterBlock_Id,
MatroskaDocType.BLOCK_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
level3 = new ElementType("ClusterBlockDuration",
(short)3,
ClusterBlockDuration_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
level3 = new ElementType("ClusterReferenceBlock",
(short)3,
ClusterReferenceBlock_Id,
ElementType.SINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
// Add ClusterBlockGroup Element
level1.children.add(level2);
level2 = new ElementType("SimpleBlock",
(short)2,
ClusterSimpleBlock_Id,
MatroskaDocType.BLOCK_ELEMENT,
new ArrayList<ElementType>());
// Add SimpleBlock Element
level1.children.add(level2);
// Add Cluster Element
level0.children.add(level1);
level1 = new ElementType("Chapters",
(short)1,
Chapters_Id,
ElementType.MASTER_ELEMENT,
new ArrayList<ElementType>());
level2 = new ElementType("ChapterEditionEntry",
(short)2,
ChapterEditionEntry_Id,
ElementType.MASTER_ELEMENT,
new ArrayList<ElementType>());
level3 = new ElementType("ChapterEditionUID",
(short)3,
ChapterEditionUID_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
level3 = new ElementType("ChapterEditionFlagHidden",
(short)3,
ChapterEditionFlagHidden_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
level3 = new ElementType("ChapterEditionFlagDefault",
(short)3,
ChapterEditionFlagDefault_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
level3 = new ElementType("ChapterEditionManaged",
(short)3,
ChapterEditionManaged_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level2.children.add(level3);
level3 = new ElementType("ChapterAtom",
(short)3,
ChapterAtom_Id,
ElementType.MASTER_ELEMENT,
new ArrayList<ElementType>());
level4 = new ElementType("ChapterAtomChapterUID",
(short)4,
ChapterAtomChapterUID_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level3.children.add(level4);
level4 = new ElementType("ChapterAtomChapterTimeStart",
(short)4,
ChapterAtomChapterTimeStart_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level3.children.add(level4);
level4 = new ElementType("ChapterAtomChapterTimeEnd",
(short)4,
ChapterAtomChapterTimeEnd_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level3.children.add(level4);
level4 = new ElementType("ChapterAtomChapterFlagHidden",
(short)4,
ChapterAtomChapterFlagHidden_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level3.children.add(level4);
level4 = new ElementType("ChapterAtomChapterFlagEnabled",
(short)4,
ChapterAtomChapterFlagEnabled_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level3.children.add(level4);
level4 = new ElementType("ChapterAtomChapterPhysicalEquiv",
(short)4,
ChapterAtomChapterPhysicalEquiv_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level3.children.add(level4);
level4 = new ElementType("ChapterAtomChapterTrack",
(short)4,
ChapterAtomChapterTrack_Id,
ElementType.MASTER_ELEMENT,
new ArrayList<ElementType>());
level5 = new ElementType("ChapterAtomChapterTrackNumber",
(short)5,
ChapterAtomChapterTrackNumber_Id,
ElementType.UINTEGER_ELEMENT,
(ArrayList<ElementType>)null);
level4.children.add(level5);
// Add ChapterAtomChapterTrack Element
level3.children.add(level4);
level4 = new ElementType("ChapterAtomChapterDisplay",
(short)4,
ChapterAtomChapterDisplay_Id,
ElementType.MASTER_ELEMENT,
new ArrayList<ElementType>());
level5 = new ElementType("ChapterAtomChapString",
(short)5,
ChapterAtomChapString_Id,
ElementType.STRING_ELEMENT,
(ArrayList<ElementType>)null);
level4.children.add(level5);
level5 = new ElementType("ChapterAtomChapLanguage",
(short)5,
ChapterAtomChapLanguage_Id,
ElementType.ASCII_STRING_ELEMENT,
(ArrayList<ElementType>)null);
level4.children.add(level5);
level5 = new ElementType("ChapterAtomChapCountry",
(short)5,
ChapterAtomChapCountry_Id,
ElementType.ASCII_STRING_ELEMENT,
(ArrayList<ElementType>)null);
level4.children.add(level5);
// Add ChapterAtomChapterDisplay Element
level3.children.add(level4);
// Add ChapterAtom Element
level2.children.add(level3);
// Add ChapterEditionEntry Element
level1.children.add(level2);
// Add Chapters Element
level0.children.add(level1);
// Add Segment Element
baseLevel.children.add(level0);
type = baseLevel;
} catch (java.lang.Exception ex) {
ex.printStackTrace();
}
}
/**
* Get the base ElementType tree.
*
* @return An ElementType that is filled with all the valid ElementType(s) for the MatroskaDocType
*/
public ElementType getElements() {
return type;
}
/**
* Creates an Element sub-class based on the ElementType.
*
* @param type ElementType to use for creation of Element.
* @return new Element sub-class, BinaryElement is the default.
* @throws RuntimeException if the ElementType has an unknown type field.
*/
public Element createElement(ElementType type) {
Element elem = null;
elem = type.createElement();
if (elem == null) {
if (type.type == MatroskaDocType.BLOCK_ELEMENT)
{
elem = new MatroskaBlock(type.id);
}
else if (type.type == MatroskaDocType.SEGMENT_ELEMENT)
{
elem = new MatroskaSegment(type.id);
}
else if (type.type == MatroskaDocType.CLUSTER_ELEMENT)
{
elem = new MatroskaCluster(type.id);
}
else if (type.type == ElementType.UNKNOWN_ELEMENT)
{
elem = new BinaryElement(type.id);
}
else
{
throw new java.lang.RuntimeException("Error: Unknown Element Type");
}
elem.setElementType(type);
}
return elem;
}
/**
* Creates an Element sub-class based on the element id.
*
* @param type id to use for creation of Element.
* @return new Element sub-class, BinaryElement is the default.
* @throws RuntimeException if the ElementType has an unknown type field.
*/
public Element createElement(byte [] type)
{
ElementType elementType = getElements().findElement(type);
if (elementType == null)
{
elementType = new UnknownElementType(type);
}
return createElement(elementType);
}
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml.matroska;
import java.util.*;
import org.ebml.*;
import org.ebml.io.*;
import org.ebml.util.*;
public class MatroskaFile {
/**
* Number of Clusters to search before assuming that a track has ended
*/
public static int CLUSTER_TRACK_SEARCH_COUNT = 4;
protected DataSource ioDS;
protected EBMLReader reader;
protected Element level0 = null;
protected String SegmentTitle;
protected Date SegmentDate;
protected String MuxingApp;
protected String WritingApp;
protected long TimecodeScale = 1000000;
protected double Duration;
public ArrayList<MatroskaFileTrack> TrackList = new ArrayList<MatroskaFileTrack>();
protected ArrayList<MatroskaFileTagEntry> TagList = new ArrayList<MatroskaFileTagEntry>();
protected TLinkedList FrameQueue = new TLinkedList();
protected boolean ScanFirstCluster = true;
public long ClusterTimecode = 0;
/**
* Primary Constructor for Matroska File class.
*
* @param inputDataSource DataSource to read the Matroska file from
*/
public MatroskaFile(DataSource inputDataSource) {
ioDS = inputDataSource;
reader = new EBMLReader(ioDS, MatroskaDocType.obj);
}
/**
* Read / Parse the Matroska file.
* Call this before any other method.
* @throws RuntimeException On various errors
*/
public void readFile() {
Element level1 = null;
Element level2 = null;
// Element level3 = null;
// Element level4 = null;
level0 = reader.readNextElement();
if (level0 == null) {
throw new java.lang.RuntimeException("Error: Unable to scan for EBML elements");
}
if (level0.equals(MatroskaDocType.EBMLHeader_Id)) {
level1 = ((MasterElement)level0).readNextChild(reader);
while (level1 != null) {
level1.readData(ioDS);
if (level1.equals(MatroskaDocType.DocType_Id)) {
String DocType = ((StringElement)level1).getValue();
if (DocType.compareTo("matroska") != 0 && DocType.compareTo("webm") != 0) {
throw new java.lang.RuntimeException("Error: DocType is not matroska, \"" + ((StringElement)level1).getValue() + "\"");
}
}
level1 = ((MasterElement)level0).readNextChild(reader);
}
} else {
throw new java.lang.RuntimeException("Error: EBML Header not the first element in the file");
}
level0 = reader.readNextElement();
if (level0.equals(MatroskaDocType.Segment_Id)) {
level1 = ((MasterElement)level0).readNextChild(reader);
while (level1 != null) {
if (level1.equals(MatroskaDocType.SegmentInfo_Id)) {
_parseSegmentInfo(level1, level2);
} else if (level1.equals(MatroskaDocType.Tracks_Id)) {
_parseTracks(level1, level2);
} else if (level1.equals(MatroskaDocType.Cluster_Id)) {
if (ScanFirstCluster)
{
_parseNextCluster(level1);
}
// Break out of this loop, we should only parse the first cluster
break;
} else if (level1.equals(MatroskaDocType.Tags_Id)) {
_parseTags(level1, level2);
}
level1.skipData(ioDS);
level1 = ((MasterElement)level0).readNextChild(reader);
}
} else {
throw new java.lang.RuntimeException("Error: Segment not the second element in the file");
}
}
/**
* Get the Next MatroskaFileFrame
*
* @return The next MatroskaFileFrame in the queue, or null if the file has ended
*/
public MatroskaFileFrame getNextFrame() {
if (FrameQueue.isEmpty()) {
_fillFrameQueue();
}
// If FrameQueue is still empty, must be the end of the file
if (FrameQueue.isEmpty()) {
return null;
}
return (MatroskaFileFrame)FrameQueue.removeFirst();
}
/**
* Get the Next MatroskaFileFrame, limited by TrackNo
*
* @param TrackNo The track number to only get MatroskaFileFrame(s) from
* @return The next MatroskaFileFrame in the queue, or null if there are no more frames for the TrackNo track
*/
public MatroskaFileFrame getNextFrame(int TrackNo) {
if (FrameQueue.isEmpty()) {
_fillFrameQueue();
}
// If FrameQueue is still empty, must be the end of the file
if (FrameQueue.isEmpty()) {
return null;
}
int tryCount = 0;
MatroskaFileFrame frame = null;
try {
TLinkedList.IteratorImpl iter = FrameQueue.first();
while (frame == null) {
if (iter.hasNext()) {
frame = (MatroskaFileFrame)iter.next();
if (frame.TrackNo == TrackNo) {
synchronized (FrameQueue) {
iter.remove();
}
return frame;
}
frame = null;
} else {
_fillFrameQueue();
// Update Iterator
int index = iter.nextIndex();
iter = FrameQueue.listIterator(index);
if (++tryCount > CLUSTER_TRACK_SEARCH_COUNT) {
// If we have not found any frames belonging to a track in 4 clusters
// there is a good chance that the track is over
return null;
}
}
}
} catch (RuntimeException ex) {
ex.printStackTrace();
return null;
}
return frame;
}
public boolean isSeekable() {
return this.ioDS.isSeekable();
}
/**
* Seek to the requested timecode, rescaning clusters and/or discarding frames
* until we reach the nearest possible timecode, rounded down.
*
* <p>
* For example<br>
* Say we have a file with 10 frames
* <table><tr><th>Frame No</th><th>Timecode</th></tr>
* <tr><td>Frame 1</td> <td>0ms</td></tr>
* <tr><td>Frame 2</td> <td>50ms</td></tr>
* <tr><td>Frame 3</td> <td>100ms</td></tr>
* <tr><td>Frame 4</td> <td>150ms</td></tr>
* <tr><td>Frame 5</td> <td>200ms</td></tr>
* <tr><td>Frame 6</td> <td>250ms</td></tr>
* <tr><td>Frame 7</td> <td>300ms</td></tr>
* <tr><td>Frame 8</td> <td>350ms</td></tr>
* <tr><td>Frame 9</td> <td>400ms</td></tr>
* <tr><td>Frame 10</td> <td>450ms</td></tr>
* </table>
* We are requested to seek to 333ms, so we discard frames until we hit an
* timecode larger than the requested. We would seek to Frame 7 at 300ms.
* </p>
*
* @param timecode Timecode to seek to in millseconds
* @return Actual timecode we seeked to
*/
public long seek(long timecode) {
return 0;
}
private void _fillFrameQueue() {
if (level0 == null)
throw new java.lang.IllegalStateException("Call readFile() before reading frames");
synchronized (level0) {
Element level1 = ((MasterElement)level0).readNextChild(reader);
while (level1 != null) {
if (level1.equals(MatroskaDocType.Cluster_Id)) {
_parseNextCluster(level1);
}
level1.skipData(ioDS);
level1 = ((MasterElement)level0).readNextChild(reader);
}
}
}
private void _parseNextCluster(Element level1) {
Element level2 = null;
Element level3 = null;
level2 = ((MasterElement)level1).readNextChild(reader);
while (level2 != null) {
if (level2.equals(MatroskaDocType.ClusterTimecode_Id)) {
level2.readData(ioDS);
ClusterTimecode = ((UnsignedIntegerElement)level2).getValue();
}else if(level2.equals(MatroskaDocType.ClusterSimpleBlock_Id)) {
MatroskaBlock block = null;
long BlockDuration = 0;
long BlockReference = 0;
block = (MatroskaBlock)level2;
block.readData(ioDS);
block.parseBlock();
MatroskaFileFrame frame = new MatroskaFileFrame();
frame.TrackNo = block.getTrackNo();
frame.Timecode = block.getAdjustedBlockTimecode(ClusterTimecode, this.TimecodeScale);
frame.Duration = BlockDuration;
frame.Reference = BlockReference;
frame.Data = block.getFrame(0);
frame.KeyFrame = block.isKeyFrame();
synchronized (FrameQueue) {
FrameQueue.addLast(new MatroskaFileFrame(frame));
}
if (block.getFrameCount() > 1) {
for (int f = 1; f < block.getFrameCount(); f++) {
frame.Data = block.getFrame(f);
synchronized (FrameQueue) {
FrameQueue.addLast(new MatroskaFileFrame(frame));
}
}
}
level2.skipData(ioDS);
} else if (level2.equals(MatroskaDocType.ClusterBlockGroup_Id)) {
MatroskaBlock block = null;
long BlockDuration = 0;
long BlockReference = 0;
level3 = ((MasterElement)level2).readNextChild(reader);
while (level3 != null) {
if (level3.equals(MatroskaDocType.ClusterBlock_Id)) {
block = (MatroskaBlock)level3;
block.readData(ioDS);
block.parseBlock();
} else if (level3.equals(MatroskaDocType.ClusterBlockDuration_Id)) {
level3.readData(ioDS);
BlockDuration = ((UnsignedIntegerElement)level3).getValue();
} else if (level3.equals(MatroskaDocType.ClusterReferenceBlock_Id)) {
level3.readData(ioDS);
BlockReference = ((SignedIntegerElement)level3).getValue();
}
level3.skipData(ioDS);
level3 = ((MasterElement)level2).readNextChild(reader);
}
if (block == null)
throw new java.lang.NullPointerException("BlockGroup element with no child Block!");
MatroskaFileFrame frame = new MatroskaFileFrame();
frame.TrackNo = block.getTrackNo();
frame.Timecode = block.getAdjustedBlockTimecode(ClusterTimecode, this.TimecodeScale);
frame.Duration = BlockDuration;
frame.Reference = BlockReference;
frame.Data = block.getFrame(0);
synchronized (FrameQueue) {
FrameQueue.addLast(new MatroskaFileFrame(frame));
}
if (block.getFrameCount() > 1) {
for (int f = 1; f < block.getFrameCount(); f++) {
frame.Data = block.getFrame(f);
/*if (badMP3Headers()) {
throw new RuntimeException("Bad Data!");
}*/
synchronized (FrameQueue) {
FrameQueue.addLast(new MatroskaFileFrame(frame));
}
/*if (badMP3Headers()) {
throw new RuntimeException("Bad Data!");
}*/
}
}
}
level2.skipData(ioDS);
level2 = ((MasterElement)level1).readNextChild(reader);
}
}
protected boolean badMP3Headers() {
TLinkedList.IteratorImpl iter = FrameQueue.listIterator();
while (iter.hasNext()) {
MatroskaFileFrame frame = (MatroskaFileFrame)iter.next();
if (frame.TrackNo == 2
&& frame.Data[3] != 0x54)
{
throw new RuntimeException("Bad MP3 Header! Index: " + iter.nextIndex());
}
}
return false;
}
private void _parseSegmentInfo(Element level1, Element level2) {
level2 = ((MasterElement)level1).readNextChild(reader);
while (level2 != null) {
if (level2.equals(MatroskaDocType.Title_Id)) {
level2.readData(ioDS);
SegmentTitle = ((StringElement)level2).getValue();
} else if (level2.equals(MatroskaDocType.DateUTC_Id)) {
level2.readData(ioDS);
SegmentDate = ((DateElement)level2).getDate();
} else if (level2.equals(MatroskaDocType.MuxingApp_Id)) {
level2.readData(ioDS);
MuxingApp = ((StringElement)level2).getValue();
} else if (level2.equals(MatroskaDocType.WritingApp_Id)) {
level2.readData(ioDS);
WritingApp = ((StringElement)level2).getValue();
} else if (level2.equals(MatroskaDocType.Duration_Id)) {
level2.readData(ioDS);
Duration = ((FloatElement)level2).getValue();
} else if (level2.equals(MatroskaDocType.TimecodeScale_Id)) {
level2.readData(ioDS);
TimecodeScale = ((UnsignedIntegerElement)level2).getValue();
}
level2.skipData(ioDS);
level2 = ((MasterElement)level1).readNextChild(reader);
}
}
private void _parseTracks(Element level1, Element level2) {
Element level3 = null;
Element level4 = null;
level2 = ((MasterElement)level1).readNextChild(reader);
while (level2 != null) {
if (level2.equals(MatroskaDocType.TrackEntry_Id)) {
MatroskaFileTrack track = new MatroskaFileTrack();
level3 = ((MasterElement)level2).readNextChild(reader);
while (level3 != null) {
if (level3.equals(MatroskaDocType.TrackNumber_Id)) {
level3.readData(ioDS);
track.TrackNo = (short)((UnsignedIntegerElement)level3).getValue();
} else if (level3.equals(MatroskaDocType.TrackUID_Id)) {
level3.readData(ioDS);
track.TrackUID = ((UnsignedIntegerElement)level3).getValue();
} else if (level3.equals(MatroskaDocType.TrackType_Id)) {
level3.readData(ioDS);
track.TrackType = (byte)((UnsignedIntegerElement)level3).getValue();
} else if (level3.equals(MatroskaDocType.TrackDefaultDuration_Id)) {
level3.readData(ioDS);
track.DefaultDuration = ((UnsignedIntegerElement)level3).getValue();
} else if (level3.equals(MatroskaDocType.TrackName_Id)) {
level3.readData(ioDS);
track.Name = ((StringElement)level3).getValue();
} else if (level3.equals(MatroskaDocType.TrackLanguage_Id)) {
level3.readData(ioDS);
track.Language = ((StringElement)level3).getValue();
} else if (level3.equals(MatroskaDocType.TrackCodecID_Id)) {
level3.readData(ioDS);
track.CodecID = ((StringElement)level3).getValue();
} else if (level3.equals(MatroskaDocType.TrackCodecPrivate_Id)) {
level3.readData(ioDS);
track.CodecPrivate = ((BinaryElement)level3).getData();
} else if (level3.equals(MatroskaDocType.TrackVideo_Id)) {
level4 = ((MasterElement)level3).readNextChild(reader);
while (level4 != null) {
if (level4.equals(MatroskaDocType.PixelWidth_Id)) {
level4.readData(ioDS);
track.Video_PixelWidth = (short)((UnsignedIntegerElement)level4).getValue();
} else if (level4.equals(MatroskaDocType.PixelHeight_Id)) {
level4.readData(ioDS);
track.Video_PixelHeight = (short)((UnsignedIntegerElement)level4).getValue();
} else if (level4.equals(MatroskaDocType.DisplayWidth_Id)) {
level4.readData(ioDS);
track.Video_DisplayWidth = (short)((UnsignedIntegerElement)level4).getValue();
} else if (level4.equals(MatroskaDocType.DisplayHeight_Id)) {
level4.readData(ioDS);
track.Video_DisplayHeight = (short)((UnsignedIntegerElement)level4).getValue();
}
level4.skipData(ioDS);
level4 = ((MasterElement)level3).readNextChild(reader);
}
} else if (level3.equals(MatroskaDocType.TrackAudio_Id)) {
level4 = ((MasterElement)level3).readNextChild(reader);
while (level4 != null) {
if (level4.equals(MatroskaDocType.SamplingFrequency_Id)) {
level4.readData(ioDS);
track.Audio_SamplingFrequency = (float)((FloatElement)level4).getValue();
} else if (level4.equals(MatroskaDocType.OutputSamplingFrequency_Id)) {
level4.readData(ioDS);
track.Audio_OutputSamplingFrequency = (float)((FloatElement)level4).getValue();
} else if (level4.equals(MatroskaDocType.Channels_Id)) {
level4.readData(ioDS);
track.Audio_Channels = (short)((UnsignedIntegerElement)level4).getValue();
} else if (level4.equals(MatroskaDocType.BitDepth_Id)) {
level4.readData(ioDS);
track.Audio_BitDepth = (byte)((UnsignedIntegerElement)level4).getValue();
}
level4.skipData(ioDS);
level4 = ((MasterElement)level3).readNextChild(reader);
}
}
level3.skipData(ioDS);
level3 = ((MasterElement)level2).readNextChild(reader);
}
TrackList.add(track);
}
level2.skipData(ioDS);
level2 = ((MasterElement)level1).readNextChild(reader);
}
}
private void _parseTags(Element level1, Element level2) {
Element level3 = null;
Element level4 = null;
level2 = ((MasterElement)level1).readNextChild(reader);
while (level2 != null) {
if (level2.equals(MatroskaDocType.Tag_Id)) {
MatroskaFileTagEntry tag = new MatroskaFileTagEntry();
level3 = ((MasterElement)level2).readNextChild(reader);
while (level3 != null) {
if (level3.equals(MatroskaDocType.TagTargets_Id)) {
level4 = ((MasterElement)level3).readNextChild(reader);
while (level4 != null) {
if (level4.equals(MatroskaDocType.TagTargetTrackUID_Id)) {
level4.readData(ioDS);
tag.TrackUID.add(new Long(((UnsignedIntegerElement)level4).getValue()));
} else if (level4.equals(MatroskaDocType.TagTargetChapterUID_Id)) {
level4.readData(ioDS);
tag.ChapterUID.add(new Long(((UnsignedIntegerElement)level4).getValue()));
} else if (level4.equals(MatroskaDocType.TagTargetAttachmentUID_Id)) {
level4.readData(ioDS);
tag.AttachmentUID.add(new Long(((UnsignedIntegerElement)level4).getValue()));
}
level4.skipData(ioDS);
level4 = ((MasterElement)level3).readNextChild(reader);
}
} else if (level3.equals(MatroskaDocType.TagSimpleTag_Id)) {
tag.SimpleTags.add(_parseTagsSimpleTag(level3, level4));
}
level3.skipData(ioDS);
level3 = ((MasterElement)level2).readNextChild(reader);
}
TagList.add(tag);
}
level2.skipData(ioDS);
level2 = ((MasterElement)level1).readNextChild(reader);
}
}
private MatroskaFileSimpleTag _parseTagsSimpleTag(Element level3, Element level4) {
MatroskaFileSimpleTag SimpleTag = new MatroskaFileSimpleTag();
level4 = ((MasterElement)level3).readNextChild(reader);
while (level4 != null) {
if (level4.equals(MatroskaDocType.TagSimpleTagName_Id)) {
level4.readData(ioDS);
SimpleTag.Name = ((StringElement)level4).getValue();
} else if (level4.equals(MatroskaDocType.TagSimpleTagString_Id)) {
level4.readData(ioDS);
SimpleTag.Value = ((StringElement)level4).getValue();
} else if (level4.equals(MatroskaDocType.TagSimpleTag_Id)) {
SimpleTag.Children.add(_parseTagsSimpleTag(level3, level4));
}
level4.skipData(ioDS);
level4 = ((MasterElement)level3).readNextChild(reader);
}
return SimpleTag;
}
/**
* Get a String report for the Matroska file.
* Call readFile() before this method, else the report will be empty.
*
* @return String Report
*/
public String getReport() {
java.io.StringWriter s = new java.io.StringWriter();
int t;
s.write("MatroskaFile report\n");
s.write("Infomation Segment \n");
s.write("\tSegment Title: " + SegmentTitle + "\n");
s.write("\tSegment Date: " + SegmentDate + "\n");
s.write("\tMuxing App : " + MuxingApp + "\n");
s.write("\tWriting App : " + WritingApp + "\n");
s.write("\tDuration : " + Duration/1000 + "sec \n");
s.write("\tTimecodeScale : " + TimecodeScale + "\n");
s.write("Track Count: " + TrackList.size() + "\n");
for (t = 0; t < TrackList.size(); t++) {
s.write("\tTrack " + t + "\n");
s.write(TrackList.get(t).toString());
}
s.write("Tag Count: " + TagList.size() + "\n");
for (t = 0; t < TagList.size(); t++) {
s.write("\tTag Entry \n");
s.write(TagList.get(t).toString());
}
s.write("End report\n");
return s.getBuffer().toString();
}
public String getWritingApp() {
return WritingApp;
}
/**
* Returns an array of the tracks.
* If there are no MatroskaFileTracks to return the returned array
* will have a size of 0.
*
* @return Array of MatroskaFileTrack's
*/
public MatroskaFileTrack [] getTrackList() {
if (TrackList.size() > 0) {
MatroskaFileTrack [] tracks = new MatroskaFileTrack[TrackList.size()];
for (int t = 0; t < TrackList.size(); t++) {
tracks[t] = (MatroskaFileTrack)TrackList.get(t);
}
return tracks;
} else {
return new MatroskaFileTrack[0];
}
}
/**
* <p>
* This differs from the getTrackList method in that this method scans
* each track and returns the one that has the same track number as TrackNo.
* </p>
*
* <p>Note: TrackNo != track index</p>
*
* @param TrackNo The actual track number of the MatroskaFileTrack you would like to get
* @return null if no MatroskaFileTrack is found with the requested TrackNo
*/
public MatroskaFileTrack getTrack(int TrackNo) {
for (int t = 0; t < TrackList.size(); t++) {
MatroskaFileTrack track = (MatroskaFileTrack)TrackList.get(t);
if (track.TrackNo == TrackNo)
return track;
}
return null;
}
/**
* Get the timecode scale for this MatroskaFile.
* In Matroska the timecodes are stored scaled by this value.
* However any MatroskaFileFrame you get through the methods of this class
* will already have the timecodes correctly scaled to millseconds.
*
* @return TimecodeScale
*/
public long getTimecodeScale() {
return TimecodeScale;
}
public String getSegmentTitle() {
return SegmentTitle;
}
public String getMuxingApp() {
return MuxingApp;
}
/**
* Get the duration for this MatroskaFile.
* This is the duration value stored in the segment info.
* Which may or may not be the exact length of all, some, or one of the tracks.
*
* @return Duration in seconds
*/
public double getDuration() {
return Duration;
}
/**
* Sets if the readFile() method should scan the first cluster for infomation.
* Set to false for faster parsing.
*/
public void setScanFirstCluster(boolean scanFirstCluster)
{
ScanFirstCluster = scanFirstCluster;
}
/**
* Gets if the readFile() method should scan the first cluster for infomation.
* When set to false parsing is slightly faster.
*/
public boolean getScanFirstCluster()
{
return ScanFirstCluster;
}
}
package org.ebml.matroska;
import java.io.*;
/**
* <p>Title: JEBML</p>
* <p>Description: Java Classes to Read EBML Elements</p>
* <p>Copyright: Copyright (c) 2002-2004 John Cannon <spyder@matroska.org>, Jory Stone <jcsston@toughguy.net></p>
* <p>Company: </p>
* @author jcsston
* @version 1.0
*/
public class MatroskaFileFilter extends javax.swing.filechooser.FileFilter {
public MatroskaFileFilter() {
}
public boolean accept(File parm1) {
if (parm1.isDirectory())
return true;
String path = parm1.getAbsolutePath();
path = path.toLowerCase();
if (path.endsWith("mkv")
|| path.endsWith("mka")
|| path.endsWith("mks"))
return true;
return false;
}
public String getDescription() {
return "Matroska Video/Audio Files";
}
}
\ No newline at end of file
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml.matroska;
import org.ebml.util.*;
/**
* Matroska Frame, holds a Matroska frame timecode, duration, and data
*/
public class MatroskaFileFrame
{
/**
* Matroska Frame Puller interface
*/
public interface MatroskaFramePuller
{
public void PushNewMatroskaFrame(MatroskaFileFrame frame);
};
/**
* The track this frame belongs to
*/
public int TrackNo;
/**
* A timecode, it should be in ms
*/
public long Timecode;
/**
* The duration of this frame, it should also be in ms
*/
public long Duration;
/**
* The first reference this frame has, set to 0 for no reference
*/
public long Reference;
/**
* More references, can be null if there are no more references
*/
public long [] References;
/**
* The frame data
*/
public byte [] Data;
public boolean KeyFrame;
/**
* MatroskaFrame Default constructor
*/
public MatroskaFileFrame()
{
//System.out.println("new " + this);
}
/**
* MatroskaFrame Copy constructor
* @param copy MatroskaFrame to copy
*/
public MatroskaFileFrame(MatroskaFileFrame copy)
{
//System.out.println("MatroskaFrame copy " + this);
this.TrackNo = copy.TrackNo;
this.Timecode = copy.Timecode;
this.Duration = copy.Duration;
this.Reference = copy.Reference;
this.KeyFrame = copy.KeyFrame;
if (copy.References != null)
{
this.References = new long[copy.References.length];
ArrayCopy.arraycopy(copy.References, 0, this.References, 0, copy.References.length);
}
if (copy.Data != null)
{
this.Data = new byte[copy.Data.length];
ArrayCopy.arraycopy(copy.Data, 0, this.Data, 0, copy.Data.length);
}
}
public boolean isKeyFrame() {
return KeyFrame;
}
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml.matroska;
import java.util.ArrayList;
public class MatroskaFileSimpleTag
{
public String Name;
public String Value;
public ArrayList<MatroskaFileSimpleTag> Children = new ArrayList<MatroskaFileSimpleTag>();
public String toString(int depth)
{
String s = new String();
String depthIndent = new String();
for (int d = 0; d < depth; d++)
depthIndent += "\t";
s += depthIndent + "SimpleTag\n";
s += depthIndent + "\tName: " + Name + "\n";
s += depthIndent + "\tValue: " + Value + "\n";
depth++;
for (int t = 0; t < Children.size(); t++)
{
s += ((MatroskaFileSimpleTag)Children.get(t)).toString(depth);
}
return s;
}
}
\ No newline at end of file
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml.matroska;
import java.util.ArrayList;
public class MatroskaFileTagEntry
{
public ArrayList<Long> TrackUID = new ArrayList<Long>();
public ArrayList<Long> ChapterUID = new ArrayList<Long>();
public ArrayList<Long> AttachmentUID = new ArrayList<Long>();
public ArrayList<MatroskaFileSimpleTag> SimpleTags = new ArrayList<MatroskaFileSimpleTag>();
public String toString()
{
String s = new String();
if (TrackUID.size() > 0)
{
s += "\t\t" + "TrackUID: " + TrackUID.toArray().toString() + "\n";
}
if (ChapterUID.size() > 0)
{
s += "\t\t" + "ChapterUID: " + ChapterUID.toArray().toString() + "\n";
}
if (AttachmentUID.size() > 0)
{
s += "\t\t" + "AttachmentUID: " + AttachmentUID.toArray().toString() + "\n";
}
for (int t = 0; t < SimpleTags.size(); t++)
{
s += ((MatroskaFileSimpleTag)SimpleTags.get(t)).toString(2);
}
return s;
}
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml.matroska;
/**
* Matroska Track Class
*/
public class MatroskaFileTrack
{
public short TrackNo;
public long TrackUID;
public byte TrackType;
public long DefaultDuration;
public String Name;
public String Language;
public String CodecID;
public byte [] CodecPrivate;
public short Video_PixelWidth;
public short Video_PixelHeight;
public short Video_DisplayWidth;
public short Video_DisplayHeight;
public float Audio_SamplingFrequency;
public float Audio_OutputSamplingFrequency;
public short Audio_Channels;
public byte Audio_BitDepth;
/**
* Converts the Track to String form
* @return String form of MatroskaFileTrack data
*/
public String toString()
{
String s = new String();
s += "\t\t" + "TrackNo: " + TrackNo + "\n";
s += "\t\t" + "TrackUID: " + TrackUID + "\n";
s += "\t\t" + "TrackType: " + MatroskaDocType.TrackTypeToString(TrackType) + "\n";
s += "\t\t" + "DefaultDuration: " + DefaultDuration + "\n";
s += "\t\t" + "Name: " + Name + "\n";
s += "\t\t" + "Language: " + Language + "\n";
s += "\t\t" + "CodecID: " + CodecID + "\n";
if (CodecPrivate != null)
s += "\t\t" + "CodecPrivate: " + CodecPrivate.length + " byte(s)" + "\n";
if (TrackType == MatroskaDocType.track_video)
{
s += "\t\t" + "PixelWidth: " + Video_PixelWidth + "\n";
s += "\t\t" + "PixelHeight: " + Video_PixelHeight + "\n";
s += "\t\t" + "DisplayWidth: " + Video_DisplayWidth + "\n";
s += "\t\t" + "DisplayHeight: " + Video_DisplayHeight + "\n";
}
if (TrackType == MatroskaDocType.track_audio)
{
s += "\t\t" + "SamplingFrequency: " + Audio_SamplingFrequency + "\n";
if (Audio_OutputSamplingFrequency != 0)
s += "\t\t" + "OutputSamplingFrequency: " + Audio_OutputSamplingFrequency + "\n";
s += "\t\t" + "Channels: " + Audio_Channels + "\n";
if (Audio_BitDepth != 0)
s += "\t\t" + "BitDepth: " + Audio_BitDepth + "\n";
}
return s;
}
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml.matroska;
import java.util.ArrayList;
import java.util.Date;
import org.ebml.BinaryElement;
import org.ebml.DateElement;
import org.ebml.FloatElement;
import org.ebml.MasterElement;
import org.ebml.StringElement;
import org.ebml.UnsignedIntegerElement;
import org.ebml.io.DataWriter;
/**
* Summary description for MatroskaFileWriter.
*/
public class MatroskaFileWriter
{
protected DataWriter ioDW;
private MatroskaCluster clusterElem = null;
private long clusterTimecode;
protected MatroskaDocType doc = new MatroskaDocType();
public long TimecodeScale = 1000000;
public double Duration = 60.0;
public Date SegmentDate = new Date();
public ArrayList<MatroskaFileTrack> TrackList = new ArrayList<MatroskaFileTrack>();
public MatroskaFileWriter(DataWriter outputDataWriter)
{
ioDW = outputDataWriter;
}
public void writeEBMLHeader()
{
MasterElement ebmlHeaderElem = (MasterElement)doc.createElement(MatroskaDocType.EBMLHeader_Id);
UnsignedIntegerElement ebml1 = (UnsignedIntegerElement)doc.createElement(MatroskaDocType.EBMLVersion_Id);
ebml1.setValue(1);
UnsignedIntegerElement ebml2 = (UnsignedIntegerElement)doc.createElement(MatroskaDocType.EBMLReadVersion_Id);
ebml2.setValue(1);
UnsignedIntegerElement ebml3 = (UnsignedIntegerElement)doc.createElement(MatroskaDocType.EBMLMaxIDLength_Id);
ebml3.setValue(4);
UnsignedIntegerElement ebml4 = (UnsignedIntegerElement)doc.createElement(MatroskaDocType.EBMLMaxSizeLength_Id);
ebml4.setValue(8);
StringElement docTypeElem = (StringElement)doc.createElement(MatroskaDocType.DocType_Id);
docTypeElem.setValue("webm");
UnsignedIntegerElement docTypeVersionElem = (UnsignedIntegerElement)doc.createElement(MatroskaDocType.DocTypeVersion_Id);
docTypeVersionElem.setValue(2);
UnsignedIntegerElement docTypeReadVersionElem = (UnsignedIntegerElement)doc.createElement(MatroskaDocType.DocTypeReadVersion_Id);
docTypeReadVersionElem.setValue(2);
ebmlHeaderElem.addChildElement(ebml1);
ebmlHeaderElem.addChildElement(ebml2);
ebmlHeaderElem.addChildElement(ebml3);
ebmlHeaderElem.addChildElement(ebml4);
ebmlHeaderElem.addChildElement(docTypeElem);
ebmlHeaderElem.addChildElement(docTypeVersionElem);
ebmlHeaderElem.addChildElement(docTypeReadVersionElem);
ebmlHeaderElem.writeElement(ioDW);
}
public void writeSegmentHeader()
{
MatroskaSegment segmentElem = (MatroskaSegment)doc.createElement(MatroskaDocType.Segment_Id);
//segmentElem.setSize(-1);
segmentElem.setUnknownSize(true);
segmentElem.writeHeaderData(ioDW);
}
public void writeSegmentInfo()
{
MasterElement segmentInfoElem = (MasterElement)doc.createElement(MatroskaDocType.SegmentInfo_Id);
StringElement writingAppElem = (StringElement)doc.createElement(MatroskaDocType.WritingApp_Id);
writingAppElem.setValue("Matroska File Writer v1.0");
StringElement muxingAppElem = (StringElement)doc.createElement(MatroskaDocType.MuxingApp_Id);
muxingAppElem.setValue("JEBML v1.0");
DateElement dateElem = (DateElement)doc.createElement(MatroskaDocType.DateUTC_Id);
dateElem.setDate(SegmentDate);
//Add timecode scale
UnsignedIntegerElement timecodescaleElem = (UnsignedIntegerElement)doc.createElement(MatroskaDocType.TimecodeScale_Id);
timecodescaleElem.setValue(TimecodeScale);
FloatElement durationElem = (FloatElement)doc.createElement(MatroskaDocType.Duration_Id);
durationElem.setValue(Duration * 1000.0);
segmentInfoElem.addChildElement(dateElem);
segmentInfoElem.addChildElement(timecodescaleElem);
segmentInfoElem.addChildElement(durationElem);
segmentInfoElem.addChildElement(writingAppElem);
segmentInfoElem.addChildElement(muxingAppElem);
segmentInfoElem.writeElement(ioDW);
}
public void writeTracks()
{
MasterElement tracksElem = (MasterElement)doc.createElement(MatroskaDocType.Tracks_Id);
for (int i = 0; i < TrackList.size(); i++)
{
MatroskaFileTrack track = (MatroskaFileTrack)TrackList.get(i);
MasterElement trackEntryElem = (MasterElement)doc.createElement(MatroskaDocType.TrackEntry_Id);
UnsignedIntegerElement trackNoElem = (UnsignedIntegerElement)doc.createElement(MatroskaDocType.TrackNumber_Id);
trackNoElem.setValue(track.TrackNo);
UnsignedIntegerElement trackUIDElem = (UnsignedIntegerElement)doc.createElement(MatroskaDocType.TrackUID_Id);
trackUIDElem.setValue(track.TrackUID);
UnsignedIntegerElement trackTypeElem = (UnsignedIntegerElement)doc.createElement(MatroskaDocType.TrackType_Id);
trackTypeElem.setValue(track.TrackType);
StringElement trackNameElem = (StringElement)doc.createElement(MatroskaDocType.TrackName_Id);
trackNameElem.setValue(track.Name);
StringElement trackLangElem = (StringElement)doc.createElement(MatroskaDocType.TrackLanguage_Id);
trackLangElem.setValue(track.Language);
StringElement trackCodecIDElem = (StringElement)doc.createElement(MatroskaDocType.TrackCodecID_Id);
trackCodecIDElem.setValue(track.CodecID);
BinaryElement trackCodecPrivateElem = (BinaryElement)doc.createElement(MatroskaDocType.TrackCodecPrivate_Id);
trackCodecPrivateElem.setData(track.CodecPrivate);
UnsignedIntegerElement trackDefaultDurationElem = (UnsignedIntegerElement)doc.createElement(MatroskaDocType.TrackDefaultDuration_Id);
trackDefaultDurationElem.setValue(track.DefaultDuration);
trackEntryElem.addChildElement(trackNoElem);
trackEntryElem.addChildElement(trackUIDElem);
trackEntryElem.addChildElement(trackTypeElem);
trackEntryElem.addChildElement(trackNameElem);
trackEntryElem.addChildElement(trackLangElem);
trackEntryElem.addChildElement(trackCodecIDElem);
trackEntryElem.addChildElement(trackCodecPrivateElem);
trackEntryElem.addChildElement(trackDefaultDurationElem);
// Now we add the audio/video dependant sub-elements
if (track.TrackType == MatroskaDocType.track_video)
{
MasterElement trackVideoElem = (MasterElement)doc.createElement(MatroskaDocType.TrackVideo_Id);
UnsignedIntegerElement trackVideoPixelWidthElem = (UnsignedIntegerElement)doc.createElement(MatroskaDocType.PixelWidth_Id);
trackVideoPixelWidthElem.setValue(track.Video_PixelWidth);
UnsignedIntegerElement trackVideoPixelHeightElem = (UnsignedIntegerElement)doc.createElement(MatroskaDocType.PixelHeight_Id);
trackVideoPixelHeightElem.setValue(track.Video_PixelHeight);
UnsignedIntegerElement trackVideoDisplayWidthElem = (UnsignedIntegerElement)doc.createElement(MatroskaDocType.DisplayWidth_Id);
trackVideoDisplayWidthElem.setValue(track.Video_DisplayWidth);
UnsignedIntegerElement trackVideoDisplayHeightElem = (UnsignedIntegerElement)doc.createElement(MatroskaDocType.DisplayHeight_Id);
trackVideoDisplayHeightElem.setValue(track.Video_DisplayHeight);
trackVideoElem.addChildElement(trackVideoPixelWidthElem);
trackVideoElem.addChildElement(trackVideoPixelHeightElem);
trackVideoElem.addChildElement(trackVideoDisplayWidthElem);
trackVideoElem.addChildElement(trackVideoDisplayHeightElem);
trackEntryElem.addChildElement(trackVideoElem);
}
else if (track.TrackType == MatroskaDocType.track_audio)
{
MasterElement trackAudioElem = (MasterElement)doc.createElement(MatroskaDocType.TrackVideo_Id);
UnsignedIntegerElement trackAudioChannelsElem = (UnsignedIntegerElement)doc.createElement(MatroskaDocType.Channels_Id);
trackAudioChannelsElem.setValue(track.Audio_Channels);
UnsignedIntegerElement trackAudioBitDepthElem = (UnsignedIntegerElement)doc.createElement(MatroskaDocType.BitDepth_Id);
trackAudioBitDepthElem.setValue(track.Audio_BitDepth);
FloatElement trackAudioSamplingRateElem = (FloatElement)doc.createElement(MatroskaDocType.SamplingFrequency_Id);
trackAudioSamplingRateElem.setValue(track.Audio_SamplingFrequency);
FloatElement trackAudioOutputSamplingFrequencyElem = (FloatElement)doc.createElement(MatroskaDocType.OutputSamplingFrequency_Id);
trackAudioOutputSamplingFrequencyElem.setValue(track.Audio_OutputSamplingFrequency);
trackAudioElem.addChildElement(trackAudioChannelsElem);
trackAudioElem.addChildElement(trackAudioBitDepthElem);
trackAudioElem.addChildElement(trackAudioSamplingRateElem);
trackAudioElem.addChildElement(trackAudioOutputSamplingFrequencyElem);
trackEntryElem.addChildElement(trackAudioElem);
}
tracksElem.addChildElement(trackEntryElem);
}
tracksElem.writeElement(ioDW);
}
public void startCluster(long clusterTimecode)
{
clusterElem = (MatroskaCluster)doc.createElement(MatroskaDocType.Cluster_Id);
UnsignedIntegerElement clusterTimecodeElem = (UnsignedIntegerElement)doc.createElement(MatroskaDocType.ClusterTimecode_Id);
clusterTimecodeElem.setValue(clusterTimecode);
clusterElem.addChildElement(clusterTimecodeElem);
this.clusterTimecode = clusterTimecode;
}
public void endCluster()
{
if (clusterElem != null) clusterElem.writeElement(ioDW);
}
/**
* Add a frame
*
* @param frame The frame to add
*/
public void addFrame(MatroskaFileFrame frame)
{
MatroskaBlock simpleBlockElem = (MatroskaBlock)doc.createElement(MatroskaDocType.ClusterSimpleBlock_Id);
simpleBlockElem.setFrameData(frame.TrackNo, frame.Timecode - clusterTimecode, frame.Data);
clusterElem.addChildElement(simpleBlockElem);
}
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml.matroska;
import org.ebml.*;
import org.ebml.io.*;
/**
* Summary description for MatroskaSegment.
*/
public class MatroskaSegment extends MasterElement
{
protected boolean bUnknownSize = false;
public MatroskaSegment(byte[] type)
{
super(type);
}
/** Write the element header data.
* The override will write the size as unknown if the flag is set.
*/
public long writeHeaderData(DataWriter writer)
{
long len = 0;
byte [] type = getType();
len += type.length;
writer.write(type);
byte [] size;
if (bUnknownSize)
{
size = new byte[5];
size[0] = (byte)(0xFF >>> (size.length-1));
for (int i = 1; i < size.length; i++)
size[i] = (byte)0xFF;
}
else
{
size = Element.makeEbmlCodedSize(getSize());
}
len += size.length;
writer.write(size);
return len;
}
/**
* Setter for Unknown Size flag.
* This is a special case for ebml. The size value is filled with 1's.
*/
public void setUnknownSize(boolean bUnknownSize)
{
this.bUnknownSize = bUnknownSize;
}
/**
* Getter for Unknown Size flag.
*/
public boolean getUnknownSize()
{
return bUnknownSize;
}
}
package org.ebml.sample;
import java.io.FileOutputStream;
import java.io.IOException;
import org.ebml.io.FileDataSource;
import org.ebml.io.FileDataWriter;
import org.ebml.matroska.MatroskaFile;
import org.ebml.matroska.MatroskaFileFrame;
import org.ebml.matroska.MatroskaFileTrack;
import org.ebml.matroska.MatroskaFileWriter;
/**
* <p>Title: JEBML</p>
* <p>Description: Java Classes to Read EBML Elements</p>
* <p>Copyright: Copyright (c) 2002-2004 John Cannon <spyder@matroska.org>, Jory Stone <jcsston@toughguy.net></p>
* <p>Company: </p>
* @author jcsston
* @version 1.0
*/
public class CommandLineSample {
public static void main(String[] args) {
System.out.println("JEBML CommandLineSample - (c) 2004 Jory 'jcsston' Stone <jcsston@toughguy.net>");
if (args.length < 3) {
System.out.println("Please provide a command and matroska filename on the command-line");
return;
}
try {
String mode = args[1];
if (mode.compareTo("-i") == 0)
{
readFile(args[2]);
}
else if (mode.compareTo("-o") == 0)
{
writeFile(args[2]);
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
static void readFile(String filename) throws IOException
{
System.out.println("Scanning file: " + filename);
long startTime = System.currentTimeMillis();
FileDataSource iFS = new FileDataSource(filename);
MatroskaFile mF = new MatroskaFile(iFS);
mF.setScanFirstCluster(true);
mF.readFile();
System.out.println(mF.getReport());
MatroskaFileTrack track = mF.getTrack(1);
if (track.CodecID.compareTo("A_MPEG/L3") == 0)
{
System.out.println("Extracting mp3 track");
String outputFilename = filename + ".mp3";// + ".wav";
FileOutputStream oFS = new FileOutputStream(outputFilename);
/*
WavLib.WaveFormatEx wfx = new WavLib.WaveFormatEx();
wfx.wFormatTag = 0x55; // MP3
wfx.nSamplesPerSec = (int)track.Audio_SamplingFrequency;
if (track.Audio_Channels == 0) {
wfx.nChannels = 1;
} else {
wfx.nChannels = track.Audio_Channels;
}
if (track.Audio_BitDepth == 0) {
wfx.wBitsPerSample = 16;
} else {
wfx.wBitsPerSample = track.Audio_BitDepth;
}
//wfx.nBlockAlign = 4;
WavLib.WavWriter writer = new WavLib.WavWriter();
writer.Open(outputFilename, wfx);
*/
MatroskaFileFrame frame = mF.getNextFrame();
while (frame != null)
{
oFS.write(frame.Data);
//writer.WriteSampleData(frame.Data, 0, frame.Data.length);
frame = mF.getNextFrame();
}
//writer.Close();
}
long endTime = System.currentTimeMillis();
System.out.println("Scan complete. Took: " + ((endTime - startTime) / 1000.0) + " seconds");
}
static void writeFile(String filename) throws IOException
{
System.out.println("Write file: " + filename);
FileDataWriter iFW = new FileDataWriter(filename);
MatroskaFileWriter mFW = new MatroskaFileWriter(iFW);
mFW.writeEBMLHeader();
mFW.writeSegmentHeader();
mFW.writeSegmentInfo();
for (int i = 0; i < 5; i++)
{
MatroskaFileTrack track = new MatroskaFileTrack();
track.TrackNo = (short)i;
track.TrackUID = new java.util.Random().nextLong();
track.TrackType = 1;
track.Name = "Track " + Integer.toString(i);
track.Video_PixelWidth = 320;
track.Video_PixelHeight = 240;
mFW.TrackList.add(track);
}
mFW.writeTracks();
System.out.println("Write complete");
}
}
package org.ebml.sample;
import java.awt.*;
import javax.swing.*;
/**
* <p>Title: EBMLReader</p>
* <p>Description: Java Classes to Read EBML Elements</p>
* <p>Copyright: Copyright (c) 2004 Jory Stone <jcsston@toughguy.net></p>
* <p>Company: </p>
* @author jcsston
* @version 1.0
*/
public class EbmlSampleApp {
boolean packFrame = false;
//Construct the application
public EbmlSampleApp() {
EbmlSampleAppFrame frame = new EbmlSampleAppFrame();
//Validate frames that have preset sizes
//Pack frames that have useful preferred size info, e.g. from their layout
if (packFrame) {
frame.pack();
}
else {
frame.validate();
}
//Center the window
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
Dimension frameSize = frame.getSize();
if (frameSize.height > screenSize.height) {
frameSize.height = screenSize.height;
}
if (frameSize.width > screenSize.width) {
frameSize.width = screenSize.width;
}
frame.setLocation((screenSize.width - frameSize.width) / 2,
(screenSize.height - frameSize.height) / 2);
frame.setVisible(true);
}
//Main method
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
}
catch (Exception e) {
e.printStackTrace();
}
new EbmlSampleApp();
}
}
\ No newline at end of file
package org.ebml.sample;
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import org.ebml.io.*;
import org.ebml.matroska.*;
/**
* <p>Title: EBMLReader</p>
* <p>Description: Java Classes to Read EBML Elements</p>
* <p>Copyright: Copyright (c) 2004 Jory Stone <jcsston@toughguy.net></p>
* <p>Company: </p>
* @author jcsston
* @version 1.0
*/
public class EbmlSampleAppFrame
extends JFrame {
private static final long serialVersionUID = 1L;
JPanel contentPane;
JMenuBar jMenuBar1 = new JMenuBar();
JMenu jMenuFile = new JMenu();
JMenuItem jMenuFileExit = new JMenuItem();
BorderLayout borderLayout1 = new BorderLayout();
JMenuItem jMenuItemOpen = new JMenuItem();
JFileChooser jFileChooser1 = new JFileChooser();
JTextArea jTextArea1 = new JTextArea();
//Construct the frame
public EbmlSampleAppFrame() {
enableEvents(AWTEvent.WINDOW_EVENT_MASK);
try {
jbInit();
}
catch (Exception e) {
e.printStackTrace();
}
}
//Component initialization
private void jbInit() throws Exception {
contentPane = (JPanel)this.getContentPane();
contentPane.setLayout(borderLayout1);
this.setSize(new Dimension(400, 300));
this.setTitle("Ebml Sample App");
jMenuFile.setText("File");
jMenuFileExit.setText("Exit");
jMenuFileExit.addActionListener(new
EbmlSampleAppFrame_jMenuFileExit_ActionAdapter(this));
jMenuItemOpen.setText("Open");
jMenuItemOpen.addActionListener(new
EbmlSampleAppFrame_jMenuItemOpen_actionAdapter(this));
jFileChooser1.setAcceptAllFileFilterUsed(true);
jFileChooser1.setDialogTitle("Select a Matroska File");
jFileChooser1.setFileFilter(null);
jTextArea1.setTabSize(8);
jMenuFile.add(jMenuItemOpen);
jMenuFile.add(jMenuFileExit);
jMenuBar1.add(jMenuFile);
contentPane.add(jTextArea1, BorderLayout.CENTER);
this.setJMenuBar(jMenuBar1);
}
//File | Exit action performed
public void jMenuFileExit_actionPerformed(ActionEvent e) {
System.exit(0);
}
//Overridden so we can exit when window is closed
protected void processWindowEvent(WindowEvent e) {
super.processWindowEvent(e);
if (e.getID() == WindowEvent.WINDOW_CLOSING) {
jMenuFileExit_actionPerformed(null);
}
}
void jMenuItemOpen_actionPerformed(ActionEvent e) {
try {
jFileChooser1.setFileFilter(new org.ebml.matroska.MatroskaFileFilter());
int ret = jFileChooser1.showOpenDialog(this);
if (ret == JFileChooser.APPROVE_OPTION) {
/*
FileInputStream inFS1 = new FileInputStream("I:\\videos\\111-Videos_111\\Linkin Park - Crawling-remux.mkvmerge.mp3");
FileOutputStream outFS1 = new FileOutputStream("I:\\videos\\111-Videos_111\\Linkin Park - Crawling-remux.mkvmerge.java.mp3");
byte [] buffer = new byte[64];
int len = 64;
while (len > 0) {
len = inFS1.read(buffer);
outFS1.write(buffer, 0, len);
}
*/
//FileOutputStream outFS = new FileOutputStream(jFileChooser1.getSelectedFile() + ".mp3");
FileInputStream ioF = new FileInputStream(jFileChooser1.getSelectedFile());
jTextArea1.append("Scanning file: " + jFileChooser1.getSelectedFile().toString() + "\n");
MatroskaFile mF = new MatroskaFile(new InputStreamDataSource(ioF));
mF.readFile();
jTextArea1.append(mF.getReport());
/*MatroskaFile.MatroskaFrame frame = mF.getNextFrame(2);
while (frame != null) {
frame = mF.getNextFrame(2);
if (frame != null)
outFS.write(frame.Data);
}*/
jTextArea1.append("Scan complete.\n");
}
} catch (java.io.FileNotFoundException ex) {
jTextArea1.append("File Not Found!\n");
ex.printStackTrace();
} catch (java.lang.RuntimeException ex) {
jTextArea1.append("Error: " + ex.toString() + ex.getMessage() + "\"\n");
ex.printStackTrace();
}
}
}
class EbmlSampleAppFrame_jMenuFileExit_ActionAdapter
implements ActionListener {
EbmlSampleAppFrame adaptee;
EbmlSampleAppFrame_jMenuFileExit_ActionAdapter(EbmlSampleAppFrame adaptee) {
this.adaptee = adaptee;
}
public void actionPerformed(ActionEvent e) {
adaptee.jMenuFileExit_actionPerformed(e);
}
}
class EbmlSampleAppFrame_jMenuItemOpen_actionAdapter
implements java.awt.event.ActionListener {
EbmlSampleAppFrame adaptee;
EbmlSampleAppFrame_jMenuItemOpen_actionAdapter(EbmlSampleAppFrame adaptee) {
this.adaptee = adaptee;
}
public void actionPerformed(ActionEvent e) {
adaptee.jMenuItemOpen_actionPerformed(e);
}
}
package org.ebml.sample;
import java.io.FileOutputStream;
import java.io.IOException;
import org.ebml.io.FileDataSource;
import org.ebml.matroska.MatroskaFile;
import org.ebml.matroska.MatroskaFileFrame;
import org.ebml.matroska.MatroskaFileTrack;
/**
* <p>Title: JEBML</p>
* <p>Description: Java Classes to Extract keyframes from WebM Files as WebP Images</p>
* <p>Copyright: Copyright (c) 2011 Brooss</p>
* <p>Company: </p>
* @author brooss
* @version 1.0
*/
public class WebMExtract {
public static void main(String[] args) {
System.out.println("JEBML WebMExtract");
if (args.length < 1) {
System.out.println("Please provide a WebM filename on the command-line");
return;
}
try {
readFile(args[0]);
} catch (IOException ex) {
ex.printStackTrace();
}
}
static void readFile(String filename) throws IOException
{
System.out.println("Scanning file: " + filename);
long startTime = System.currentTimeMillis();
FileDataSource iFS = new FileDataSource(filename);
MatroskaFile mF = new MatroskaFile(iFS);
mF.setScanFirstCluster(true);
mF.readFile();
System.out.println(mF.getReport());
MatroskaFileTrack track=null;
for(MatroskaFileTrack t : mF.getTrackList() ) {
if(t.CodecID.compareTo("V_VP8")==0)
track = t;
}
if (track!=null)
{
MatroskaFileFrame frame = mF.getNextFrame(track.TrackNo);
int count=0;
while (frame != null)
{
if(frame.isKeyFrame()) {
System.out.println("Extracting VP8 frame "+count);
String outputFilename = filename + ""+count+".webp";// + ".wav";
FileOutputStream oFS = new FileOutputStream(outputFilename);
oFS.write("RIFF".getBytes());
writeIntLE(oFS, frame.Data.length+20-8);
oFS.write("WEBPVP8".getBytes());
oFS.write(0x20);
writeIntLE(oFS, (frame.Data.length+20-8)-0xc);
oFS.write(frame.Data);
}
frame = mF.getNextFrame(track.TrackNo);
count++;
}
}
long endTime = System.currentTimeMillis();
System.out.println("Scan complete. Took: " + ((endTime - startTime) / 1000.0) + " seconds");
}
public static void writeIntLE(FileOutputStream out, int value) {
try {
out.write(value & 0xFF);
out.write((value >> 8) & 0xFF);
out.write((value >> 16) & 0xFF);
out.write((value >> 24) & 0xFF);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
/**
* JEBML - Java library to read/write EBML/Matroska elements.
* Copyright (C) 2004 Jory Stone <jebml@jory.info>
* Based on Javatroska (C) 2002 John Cannon <spyder@matroska.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.ebml.util;
/**
* Static methods for copying arrays
*/
public class ArrayCopy
{
/**
* Private construct as this class only has static methods
*/
private ArrayCopy()
{
}
public static void arraycopy(byte [] src, int src_offset, byte [] dest, int dest_offset, int count)
{
for (int i = 0; i < count; i++)
{
dest[dest_offset + i] = src[src_offset + i];
}
}
public static void arraycopy(char [] src, int src_offset, char [] dest, int dest_offset, int count)
{
for (int i = 0; i < count; i++)
{
dest[dest_offset + i] = src[src_offset + i];
}
}
public static void arraycopy(short [] src, int src_offset, short [] dest, int dest_offset, int count)
{
for (int i = 0; i < count; i++)
{
dest[dest_offset + i] = src[src_offset + i];
}
}
public static void arraycopy(int [] src, int src_offset, int [] dest, int dest_offset, int count)
{
for (int i = 0; i < count; i++)
{
dest[dest_offset + i] = src[src_offset + i];
}
}
public static void arraycopy(long [] src, int src_offset, long [] dest, int dest_offset, int count)
{
for (int i = 0; i < count; i++)
{
dest[dest_offset + i] = src[src_offset + i];
}
}
public static void arraycopy(Object [] src, int src_offset, Object [] dest, int dest_offset, int count)
{
for (int i = 0; i < count; i++)
{
dest[dest_offset + i] = src[src_offset + i];
}
}
}
package org.ebml.util;
/**
* A LinkedList implementation which holds instances of type
* <tt>TLinkable</tt>.
*
* <p>Using this implementation allows you to get java.util.LinkedList
* behavior (a doubly linked list, with Iterators that support insert
* and delete operations) without incurring the overhead of creating
* <tt>Node</tt> wrapper objects for every element in your list.</p>
*
* <p>The requirement to achieve this time/space gain is that the
* Objects stored in the List implement the <tt>TLinkable</tt>
* interface.</p>
*
* <p>The limitations are that you cannot put the same object into
* more than one list or more than once in the same list. You must
* also ensure that you only remove objects that are actually in the
* list. That is, if you have an object A and lists l1 and l2, you
* must ensure that you invoke List.remove(A) on the correct list. It
* is also forbidden to invoke List.remove() with an unaffiliated
* TLinkable (one that belongs to no list): this will destroy the list
* you invoke it on.</p>
*
* <p>
* Created: Sat Nov 10 15:25:10 2001
* </p>
*
* @author Eric D. Friedman
* @version $Id: TLinkedList.java,v 1.4 2002/04/08 02:02:28 ericdf Exp $
* @see gnu.trove.TLinkable
*/
public class TLinkedList
{
class TLinkable
{
protected TLinkable _next;
protected TLinkable _prev;
protected Object _value;
public TLinkable(Object value)
{
_value = value;
}
/**
* Returns the linked list node after this one.
*
* @return a <code>TLinkable</code> value
*/
public TLinkable getNext()
{
return _next;
}
/**
* Returns the linked list node before this one.
*
* @return a <code>TLinkable</code> value
*/
public TLinkable getPrevious()
{
return _prev;
}
/**
* Sets the linked list node after this one.
*
* @param linkable a <code>TLinkable</code> value
*/
public void setNext(TLinkable linkable)
{
_next = linkable;
}
/**
* Sets the linked list node before this one.
*
* @param linkable a <code>TLinkable</code> value
*/
public void setPrevious(TLinkable linkable)
{
_prev = linkable;
}
/**
* Sets the value of this list entry
*
* @param value a <code>Object</code> value
*/
public void setValue(Object value)
{
_value = value;
}
/**
* Gets the value of this list entry
*
* @param value a <code>Object</code> value
*/
public Object getValue()
{
return _value;
}
}// TLinkable
/** the head of the list */
protected TLinkable _head;
/** the tail of the list */
protected TLinkable _tail;
/** the number of elements in the list */
protected int _size = 0;
/**
* Creates a new <code>TLinkedList</code> instance.
*
*/
public TLinkedList()
{
}
public boolean isEmpty()
{
return (_size == 0);
}
public IteratorImpl first()
{
return listIterator();
}
public IteratorImpl listIterator()
{
return listIterator(0);
}
/**
* Returns an iterator positioned at <tt>index</tt>. Assuming
* that the list has a value at that index, calling next() will
* retrieve and advance the iterator. Assuming that there is a
* value before <tt>index</tt> in the list, calling previous()
* will retrieve it (the value at index - 1) and move the iterator
* to that position. So, iterating from front to back starts at
* 0; iterating from back to front starts at <tt>size()</tt>.
*
* @param index an <code>int</code> value
* @return a <code>ListIterator</code> value
*/
public IteratorImpl listIterator(int index)
{
return new IteratorImpl(index);
}
/**
* Returns the number of elements in the list.
*
* @return an <code>int</code> value
*/
public int size()
{
return _size;
}
/**
* Inserts <tt>linkable</tt> at index <tt>index</tt> in the list.
* All values > index are shifted over one position to accomodate
* the new addition.
*
* @param index an <code>int</code> value
* @param linkable an object of type TLinkable
*/
public void add(int index, Object linkable)
{
if (index < 0 || index > size())
{
throw new IndexOutOfBoundsException("index:" + index);
}
insert(index,linkable);
}
/**
* Appends <tt>linkable</tt> to the end of the list.
*
* @param linkable an object of type TLinkable
* @return always true
*/
public boolean add(Object linkable)
{
insert(_size, linkable);
return true;
}
/**
* Inserts <tt>linkable</tt> at the head of the list.
*
* @param linkable an object of type TLinkable
*/
public void addFirst(Object linkable)
{
insert(0, linkable);
}
/**
* Adds <tt>linkable</tt> to the end of the list.
*
* @param linkable an object of type TLinkable
*/
public void addLast(Object linkable)
{
insert(size(), linkable);
}
/**
* Empties the list.
*
*/
public void clear()
{
if (null != _head)
{
for (TLinkable link = _head.getNext();
link != null;
link = link.getNext())
{
TLinkable prev = link.getPrevious();
prev.setNext(null);
link.setPrevious(null);
}
_head = _tail = null;
}
_size = 0;
}
/**
* Copies the list's contents into a native array. This will be a
* shallow copy: the Tlinkable instances in the Object[] array
* have links to one another: changing those will put this list
* into an unpredictable state. Holding a reference to one
* element in the list will prevent the others from being garbage
* collected unless you clear the next/previous links. <b>Caveat
* programmer!</b>
*
* @return an <code>Object[]</code> value
*/
public Object[] toArray()
{
Object[] o = new Object[_size];
int i = 0;
for (TLinkable link = _head; link != null; link = link.getNext())
{
o[i++] = link.getValue();
}
return o;
}
/**
* Copies the list to a native array, destroying the next/previous
* links as the copy is made. This list will be emptied after the
* copy (as if clear() had been invoked). The Object[] array
* returned will contain TLinkables that do <b>not</b> hold
* references to one another and so are less likely to be the
* cause of memory leaks.
*
* @return an <code>Object[]</code> value
*/
public Object[] toUnlinkedArray()
{
Object[] o = new Object[_size];
int i = 0;
for (TLinkable link = _head, tmp = null; link != null; i++)
{
o[i] = link.getValue();
tmp = link;
link = link.getNext();
tmp.setNext(null); // clear the links
tmp.setPrevious(null);
}
_size = 0; // clear the list
_head = _tail = null;
return o;
}
/**
* A linear search for <tt>o</tt> in the list.
*
* @param o an <code>Object</code> value
* @return a <code>boolean</code> value
*/
public boolean contains(Object o)
{
for (TLinkable link = _head; link != null; link = link.getNext())
{
if (o.equals(link))
{
return true;
}
}
return false;
}
/**
* Returns the head of the list
*
* @return an <code>Object</code> value
*/
public Object getFirst()
{
return (_head != null) ? _head.getValue() : null;
}
/**
* Returns the tail of the list.
*
* @return an <code>Object</code> value
*/
public Object getLast()
{
return (_tail != null) ? _tail.getValue() : null;
}
/**
* Remove and return the first element in the list.
*
* @return an <code>Object</code> value
*/
public Object removeFirst()
{
TLinkable o = _head;
TLinkable n = o.getNext();
o.setNext(null);
if (null != n)
{
n.setPrevious(null);
}
_head = n;
if (--_size == 0)
{
_tail = null;
}
return (o != null) ? o.getValue() : null;
}
/**
* Remove and return the last element in the list.
*
* @return an <code>Object</code> value
*/
public Object removeLast()
{
TLinkable o = _tail;
TLinkable prev = o.getPrevious();
o.setPrevious(null);
if (null != prev)
{
prev.setNext(null);
}
_tail = prev;
if (--_size == 0)
{
_head = null;
}
return (o != null) ? o.getValue() : null;
}
/**
* Implementation of index-based list insertions.
*
* @param index an <code>int</code> value
* @param linkable an object of type TLinkable
*/
protected void insert(int index, Object linkable)
{
TLinkable newLink = new TLinkable(linkable);
if (_size == 0)
{
_head = _tail = newLink; // first insertion
}
else if (index == 0)
{
newLink.setNext(_head); // insert at front
_head.setPrevious(newLink);
_head = newLink;
}
else if (index == _size)
{ // insert at back
_tail.setNext(newLink);
newLink.setPrevious(_tail);
_tail = newLink;
}
else
{
TLinkable prior = null, post = null;
// looking at the size of the list, we decide whether
// it's faster to reach `index' by traversing the
// list from the front or the back.
if (index > (_size >> 1))
{ // insert in 2nd half
// work from the tail
int pos = _size -1;
for (prior = _tail; pos > index; pos--)
{
prior = prior.getPrevious();
}
}
else
{ // insert in 1st half
// work from the head
int pos = 0;
for (prior = _head; pos < index; pos++)
{
prior = prior.getNext();
}
}
post = prior.getNext();
// insert newlink
newLink.setNext(post);
newLink.setPrevious(prior);
// adjust adjacent pointers
post.setPrevious(newLink);
prior.setNext(newLink);
}
_size++;
}
/**
* Removes the specified element from the list. Note that
* it is the caller's responsibility to ensure that the
* element does, in fact, belong to this list and not another
* instance of TLinkedList.
*
* @param o a TLinkable element already inserted in this list.
* @return true if the element was a TLinkable and removed
*/
public boolean remove(Object o)
{
TLinkable link = null;
for (link = _head; link != null; link = link.getNext())
{
if (o.equals(link))
{
break;
}
}
if (link != null)
{
TLinkable p, n;
p = link.getPrevious();
n = link.getNext();
if (n == null && p == null)
{ // emptying the list
_head = _tail = null;
}
else if (n == null)
{ // this is the tail
// make previous the new tail
link.setPrevious(null);
p.setNext(null);
_tail = p;
}
else if (p == null)
{ // this is the head
// make next the new head
link.setNext(null);
n.setPrevious(null);
_head = n;
}
else
{ // somewhere in the middle
p.setNext(n);
n.setPrevious(p);
link.setNext(null);
link.setPrevious(null);
}
_size--; // reduce size of list
return true;
}
else
{
return false;
}
}
/**
* Inserts newElement into the list immediately before current.
* All elements to the right of and including current are shifted
* over.
*
* @param current a <code>TLinkable</code> value currently in the list.
* @param newElement a <code>TLinkable</code> value to be added to
* the list.
*/
public void addBefore(TLinkable current, TLinkable newElement)
{
if (current == _head)
{
addFirst(newElement);
}
else if (current == null)
{
addLast(newElement);
}
else
{
TLinkable p = current.getPrevious();
newElement.setNext(current);
p.setNext(newElement);
newElement.setPrevious(p);
current.setPrevious(newElement);
_size++;
}
}
/**
* A ListIterator that supports additions and deletions.
*
*/
public class IteratorImpl
{
private int _nextIndex = 0;
private TLinkable _next;
private TLinkable _lastReturned;
/**
* Creates a new <code>Iterator</code> instance positioned at
* <tt>index</tt>.
*
* @param position an <code>int</code> value
*/
IteratorImpl(int position)
{
if (position < 0 || position > _size)
{
throw new IndexOutOfBoundsException();
}
_nextIndex = position;
if (position == 0)
{
_next = _head;
}
else if (position == _size)
{
_next = null;
}
else if (position < (_size >> 1))
{
int pos = 0;
for (_next = _head; pos < position; pos++)
{
_next = _next.getNext();
}
}
else
{
int pos = _size - 1;
for (_next = _tail; pos > position; pos--)
{
_next = _next.getPrevious();
}
}
}
/**
* Insert <tt>linkable</tt> at the current position of the iterator.
* Calling next() after add() will return the added object.
*
* @param linkable an object of type TLinkable
*/
public final void add(Object linkable)
{
_lastReturned = null;
_nextIndex++;
if (_size == 0)
{
TLinkedList.this.add(linkable);
}
else
{
TLinkedList.this.addBefore(_next, new TLinkable(linkable));
}
}
/**
* True if a call to next() will return an object.
*
* @return a <code>boolean</code> value
*/
public final boolean hasNext()
{
return _nextIndex != _size;
}
/**
* True if a call to previous() will return a value.
*
* @return a <code>boolean</code> value
*/
public final boolean hasPrevious()
{
return _nextIndex != 0;
}
/**
* Returns the value at the Iterator's index and advances the
* iterator.
*
* @return an <code>Object</code> value
* @exception NoSuchElementException if there is no next element
*/
public final Object next()
{
if (_nextIndex == _size)
{
throw new IndexOutOfBoundsException("NoSuchElementException");
}
_lastReturned = _next;
_next = _next.getNext();
_nextIndex++;
return (_lastReturned != null) ? _lastReturned.getValue() : null;
}
/**
* returns the index of the next node in the list (the
* one that would be returned by a call to next()).
*
* @return an <code>int</code> value
*/
public final int nextIndex()
{
return _nextIndex;
}
/**
* Returns the value before the Iterator's index and moves the
* iterator back one index.
*
* @return an <code>Object</code> value
* @exception NoSuchElementException if there is no previous element.
*/
public final Object previous()
{
if (_nextIndex == 0)
{
throw new IndexOutOfBoundsException("NoSuchElementException");
}
if (_nextIndex == _size)
{
_lastReturned = _next = _tail;
}
else
{
_lastReturned = _next = _next.getPrevious();
}
_nextIndex--;
return (_lastReturned != null) ? _lastReturned.getValue() : null;
}
/**
* Returns the previous element's index.
*
* @return an <code>int</code> value
*/
public final int previousIndex()
{
return _nextIndex - 1;
}
/**
* Removes the current element in the list and shrinks its
* size accordingly.
*
* @exception IllegalStateException neither next nor previous
* have been invoked, or remove or add have been invoked after
* the last invocation of next or previous.
*/
public final void remove()
{
if (_lastReturned == null)
{
throw new IllegalStateException("must invoke next or previous before invoking remove");
}
if (_lastReturned != _next)
{
_nextIndex--;
}
_next = _lastReturned.getNext();
TLinkedList.this.remove(_lastReturned);
_lastReturned = null;
}
/**
* Replaces the current element in the list with
* <tt>linkable</tt>
*
* @param linkable an object of type TLinkable
*/
public final void set(Object linkable)
{
if (_lastReturned == null)
{
throw new IllegalStateException();
}
TLinkable l = new TLinkable(linkable);
// need to check both, since this could be the only
// element in the list.
if (_lastReturned == _head)
{
_head = l;
}
if (_lastReturned == _tail)
{
_tail = l;
}
swap(_lastReturned, l);
_lastReturned = l;
}
/**
* Replace from with to in the list.
*
* @param from a <code>TLinkable</code> value
* @param to a <code>TLinkable</code> value
*/
private void swap(TLinkable from, TLinkable to)
{
TLinkable p = from.getPrevious();
TLinkable n = from.getNext();
if (null != p)
{
to.setPrevious(p);
p.setNext(to);
}
if (null != n)
{
to.setNext(n);
n.setPrevious(to);
}
from.setNext(null);
from.setPrevious(null);
}
}
} // TLinkedList
\ No newline at end of file
/*
var vid = new Whammy.Video();
vid.add(canvas or data url)
vid.compile()
*/
var Whammy = (function(){
// in this case, frames has a very specific meaning, which will be
// detailed once i finish writing the code
function toWebM(frames){
var info = checkFrames(frames);
var counter = 0;
var EBML = [
{
"id": 0x1a45dfa3, // EBML
"data": [
{
"data": 1,
"id": 0x4286 // EBMLVersion
},
{
"data": 1,
"id": 0x42f7 // EBMLReadVersion
},
{
"data": 4,
"id": 0x42f2 // EBMLMaxIDLength
},
{
"data": 8,
"id": 0x42f3 // EBMLMaxSizeLength
},
{
"data": "webm",
"id": 0x4282 // DocType
},
{
"data": 2,
"id": 0x4287 // DocTypeVersion
},
{
"data": 2,
"id": 0x4285 // DocTypeReadVersion
}
]
},
{
"id": 0x18538067, // Segment
"data": [
{
"id": 0x1549a966, // Info
"data": [
{
"data": 1e6, //do things in millisecs (num of nanosecs for duration scale)
"id": 0x2ad7b1 // TimecodeScale
},
{
"data": "whammy",
"id": 0x4d80 // MuxingApp
},
{
"data": "whammy",
"id": 0x5741 // WritingApp
},
{
"data": doubleToString(info.duration),
"id": 0x4489 // Duration
}
]
},
{
"id": 0x1654ae6b, // Tracks
"data": [
{
"id": 0xae, // TrackEntry
"data": [
{
"data": 1,
"id": 0xd7 // TrackNumber
},
{
"data": 1,
"id": 0x63c5 // TrackUID
},
{
"data": 0,
"id": 0x9c // FlagLacing
},
{
"data": "und",
"id": 0x22b59c // Language
},
{
"data": "V_VP8",
"id": 0x86 // CodecID
},
{
"data": "VP8",
"id": 0x258688 // CodecName
},
{
"data": 1,
"id": 0x83 // TrackType
},
{
"id": 0xe0, // Video
"data": [
{
"data": info.width,
"id": 0xb0 // PixelWidth
},
{
"data": info.height,
"id": 0xba // PixelHeight
}
]
}
]
}
]
},
{
"id": 0x1f43b675, // Cluster
"data": [
{
"data": 0,
"id": 0xe7 // Timecode
}
].concat(frames.map(function(webp){
var block = makeSimpleBlock({
discardable: 0,
frame: webp.data.slice(4),
invisible: 0,
keyframe: 1,
lacing: 0,
trackNum: 1,
timecode: Math.round(counter)
});
counter += webp.duration;
return {
data: block,
id: 0xa3
};
}))
}
]
}
];
return generateEBML(EBML)
}
// sums the lengths of all the frames and gets the duration, woo
function checkFrames(frames){
var width = frames[0].width,
height = frames[0].height,
duration = frames[0].duration;
for(var i = 1; i < frames.length; i++){
if(frames[i].width != width) throw "Frame " + (i + 1) + " has a different width";
if(frames[i].height != height) throw "Frame " + (i + 1) + " has a different height";
if(frames[i].duration < 0) throw "Frame " + (i + 1) + " has a weird duration";
duration += frames[i].duration;
}
return {
duration: duration,
width: width,
height: height
};
}
function numToBuffer(num){
var parts = [];
while(num > 0){
parts.push(num & 0xff)
num = num >> 8
}
return new Uint8Array(parts.reverse());
}
function strToBuffer(str){
// return new Blob([str]);
var arr = new Uint8Array(str.length);
for(var i = 0; i < str.length; i++){
arr[i] = str.charCodeAt(i)
}
return arr;
// this is slower
// return new Uint8Array(str.split('').map(function(e){
// return e.charCodeAt(0)
// }))
}
//sorry this is ugly, and sort of hard to understand exactly why this was done
// at all really, but the reason is that there's some code below that i dont really
// feel like understanding, and this is easier than using my brain.
function bitsToBuffer(bits){
var data = [];
var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : '';
bits = pad + bits;
for(var i = 0; i < bits.length; i+= 8){
data.push(parseInt(bits.substr(i,8),2))
}
return new Uint8Array(data);
}
function generateEBML(json){
var ebml = [];
for(var i = 0; i < json.length; i++){
var data = json[i].data;
// console.log(data);
if(typeof data == 'object') data = generateEBML(data);
if(typeof data == 'number') data = bitsToBuffer(data.toString(2));
if(typeof data == 'string') data = strToBuffer(data);
// console.log(data)
var len = data.size || data.byteLength;
var zeroes = Math.ceil(Math.ceil(Math.log(len)/Math.log(2))/8);
var size_str = len.toString(2);
var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str;
var size = (new Array(zeroes)).join('0') + '1' + padded;
//i actually dont quite understand what went on up there, so I'm not really
//going to fix this, i'm probably just going to write some hacky thing which
//converts that string into a buffer-esque thing
ebml.push(numToBuffer(json[i].id));
ebml.push(bitsToBuffer(size));
ebml.push(data)
}
return new Blob(ebml, {
type: "video/webm"
});
}
//OKAY, so the following two functions are the string-based old stuff, the reason they're
//still sort of in here, is that they're actually faster than the new blob stuff because
//getAsFile isn't widely implemented, or at least, it doesn't work in chrome, which is the
// only browser which supports get as webp
//Converting between a string of 0010101001's and binary back and forth is probably inefficient
//TODO: get rid of this function
function toBinStr_old(bits){
var data = '';
var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : '';
bits = pad + bits;
for(var i = 0; i < bits.length; i+= 8){
data += String.fromCharCode(parseInt(bits.substr(i,8),2))
}
return data;
}
function generateEBML_old(json){
var ebml = '';
for(var i = 0; i < json.length; i++){
var data = json[i].data;
if(typeof data == 'object') data = generateEBML_old(data);
if(typeof data == 'number') data = toBinStr_old(data.toString(2));
var len = data.length;
var zeroes = Math.ceil(Math.ceil(Math.log(len)/Math.log(2))/8);
var size_str = len.toString(2);
var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str;
var size = (new Array(zeroes)).join('0') + '1' + padded;
ebml += toBinStr_old(json[i].id.toString(2)) + toBinStr_old(size) + data;
}
return ebml;
}
//woot, a function that's actually written for this project!
//this parses some json markup and makes it into that binary magic
//which can then get shoved into the matroska comtainer (peaceably)
function makeSimpleBlock(data){
var flags = 0;
if (data.keyframe) flags |= 128;
if (data.invisible) flags |= 8;
if (data.lacing) flags |= (data.lacing << 1);
if (data.discardable) flags |= 1;
if (data.trackNum > 127) {
throw "TrackNumber > 127 not supported";
}
var out = [data.trackNum | 0x80, data.timecode >> 8, data.timecode & 0xff, flags].map(function(e){
return String.fromCharCode(e)
}).join('') + data.frame;
return out;
}
// here's something else taken verbatim from weppy, awesome rite?
function parseWebP(riff){
var VP8 = riff.RIFF[0].WEBP[0];
var frame_start = VP8.indexOf('\x9d\x01\x2a'); //A VP8 keyframe starts with the 0x9d012a header
for(var i = 0, c = []; i < 4; i++) c[i] = VP8.charCodeAt(frame_start + 3 + i);
var width, horizontal_scale, height, vertical_scale, tmp;
//the code below is literally copied verbatim from the bitstream spec
tmp = (c[1] << 8) | c[0];
width = tmp & 0x3FFF;
horizontal_scale = tmp >> 14;
tmp = (c[3] << 8) | c[2];
height = tmp & 0x3FFF;
vertical_scale = tmp >> 14;
return {
width: width,
height: height,
data: VP8,
riff: riff
}
}
// i think i'm going off on a riff by pretending this is some known
// idiom which i'm making a casual and brilliant pun about, but since
// i can't find anything on google which conforms to this idiomatic
// usage, I'm assuming this is just a consequence of some psychotic
// break which makes me make up puns. well, enough riff-raff (aha a
// rescue of sorts), this function was ripped wholesale from weppy
function parseRIFF(string){
var offset = 0;
var chunks = {};
while (offset < string.length) {
var id = string.substr(offset, 4);
var len = parseInt(string.substr(offset + 4, 4).split('').map(function(i){
var unpadded = i.charCodeAt(0).toString(2);
return (new Array(8 - unpadded.length + 1)).join('0') + unpadded
}).join(''),2);
var data = string.substr(offset + 4 + 4, len);
offset += 4 + 4 + len;
chunks[id] = chunks[id] || [];
if (id == 'RIFF' || id == 'LIST') {
chunks[id].push(parseRIFF(data));
} else {
chunks[id].push(data);
}
}
return chunks;
}
// here's a little utility function that acts as a utility for other functions
// basically, the only purpose is for encoding "Duration", which is encoded as
// a double (considerably more difficult to encode than an integer)
function doubleToString(num){
return [].slice.call(
new Uint8Array(
(
new Float64Array([num]) //create a float64 array
).buffer) //extract the array buffer
, 0) // convert the Uint8Array into a regular array
.map(function(e){ //since it's a regular array, we can now use map
return String.fromCharCode(e) // encode all the bytes individually
})
.reverse() //correct the byte endianness (assume it's little endian for now)
.join('') // join the bytes in holy matrimony as a string
}
function WhammyVideo(speed, quality){ // a more abstract-ish API
this.frames = [];
this.duration = 1000 / speed;
this.quality = quality || 0.8;
}
WhammyVideo.prototype.add = function(frame, duration){
if(typeof duration != 'undefined' && this.duration) throw "you can't pass a duration if the fps is set";
if('canvas' in frame){ //CanvasRenderingContext2D
frame = frame.canvas;
}
if('toDataURL' in frame){
frame = frame.toDataURL('image/webp', this.quality)
}else if(typeof frame != "string"){
throw "frame must be a a HTMLCanvasElement, a CanvasRenderingContext2D or a DataURI formatted string"
}
if (!(/^data:image\/webp;base64,/ig).test(frame)) {
throw "Input must be formatted properly as a base64 encoded DataURI of type image/webp";
}
this.frames.push({
image: frame,
duration: duration || this.duration
})
}
WhammyVideo.prototype.compile = function(){
return new toWebM(this.frames.map(function(frame){
var webp = parseWebP(parseRIFF(atob(frame.image.slice(23))));
webp.duration = frame.duration;
return webp;
}))
}
return {
Video: WhammyVideo,
fromImageArray: function(images, fps){
return toWebM(images.map(function(image){
var webp = parseWebP(parseRIFF(atob(image.slice(23))))
webp.duration = 1000 / fps;
return webp;
}))
},
toWebM: toWebM
// expose methods of madness
}
})()
\ No newline at end of file
...@@ -23,7 +23,7 @@ public class BitAssistant ...@@ -23,7 +23,7 @@ public class BitAssistant
{ {
byte bytes[] = new byte[arrayList.size()]; byte bytes[] = new byte[arrayList.size()];
for(int i = 0; i < arrayList.size(); i++) for(int i = 0; i < arrayList.size(); i++)
bytes[i] = ((Byte)arrayList.get(i)).byteValue(); if (arrayList.get(i) != null) bytes[i] = ((Byte)arrayList.get(i)).byteValue();
return bytes; return bytes;
} }
...@@ -40,8 +40,9 @@ public class BitAssistant ...@@ -40,8 +40,9 @@ public class BitAssistant
public static byte[] bytesFromArray(Byte array[]) public static byte[] bytesFromArray(Byte array[])
{ {
byte bytes[] = new byte[array.length]; byte bytes[] = new byte[array.length];
for(int i = 0; i < array.length; i++) for(int i = 0; i < array.length; i++)
bytes[i] = array[i].byteValue(); if (array[i] != null) bytes[i] = array[i].byteValue();
return bytes; return bytes;
} }
......
package org.ifsoft;
import java.util.ArrayList;
import java.util.Iterator;
public class ByteCollection
{
private ArrayList _list;
public void add(byte b)
{
_list.add(Byte.valueOf(b));
}
public void add(int b)
{
_list.add(Byte.valueOf((byte)b));
}
public void addRange(byte buffer[])
{
byte arr$[] = buffer;
int len$ = arr$.length;
for(int i$ = 0; i$ < len$; i$++)
{
byte b = arr$[i$];
add(b);
}
}
public void addRange(ByteCollection collection)
{
byte b;
for(Iterator i$ = collection.getList().iterator(); i$.hasNext(); add(b))
b = ((Byte)i$.next()).byteValue();
}
public ByteCollection()
{
_list = new ArrayList();
}
public ByteCollection(byte buffer[])
{
_list = new ArrayList();
addRange(buffer);
}
public byte get(Integer index)
{
return ((Byte)_list.get(index.intValue())).byteValue();
}
public Integer getCount()
{
return Integer.valueOf(_list.size());
}
ArrayList getList()
{
return _list;
}
public byte[] getRange(Integer index, Integer count)
{
byte range[] = new byte[count.intValue()];
for(int i = 0; i < count.intValue(); i++)
range[i] = ((Byte)_list.get(index.intValue() + i)).byteValue();
return range;
}
public void insertRange(Integer index, byte buffer[])
{
for(int i = 0; i < buffer.length; i++)
_list.add(index.intValue() + i, Byte.valueOf(buffer[i]));
}
public void insertRange(Integer index, ByteCollection collection)
{
for(int i = 0; i < collection.getCount().intValue(); i++)
_list.add(index.intValue() + i, Byte.valueOf(collection.get(Integer.valueOf(i))));
}
public void removeRange(Integer index, Integer count)
{
ArrayListExtensions.removeRange(_list, index.intValue(), count.intValue());
}
public byte[] toArray()
{
byte array[] = new byte[_list.size()];
for(int i = 0; i < array.length; i++)
array[i] = ((Byte)_list.get(i)).byteValue();
return array;
}
}
package org.ifsoft;
public class Math
{
public Math()
{
}
public static Integer max(Integer val1, Integer val2)
{
return Integer.valueOf(Math.max(val1.intValue(), val2.intValue()));
}
public static Long max(Long val1, Long val2)
{
return Long.valueOf(Math.max(val1.longValue(), val2.longValue()));
}
public static Integer min(Integer val1, Integer val2)
{
return Integer.valueOf(Math.min(val1.intValue(), val2.intValue()));
}
public static Long min(Long val1, Long val2)
{
return Long.valueOf(Math.min(val1.longValue(), val2.longValue()));
}
public static Double pow(Double val, Double exp)
{
return Double.valueOf(Math.pow(val.doubleValue(), exp.doubleValue()));
}
public static Double ceiling(Double val)
{
return Double.valueOf(java.lang.Math.ceil(val.doubleValue()));
}
}
package org.ifsoft.rtp;
import org.ifsoft.*;
import java.util.ArrayList;
public class Vp8Accumulator
{
public void add(Vp8Packet packet)
{
//if(packet.getStartOfPartition().booleanValue() || ArrayListExtensions.getCount(packets).intValue() > 0)
packets.add(packet);
}
public Vp8Accumulator()
{
packets = new ArrayList();
}
public Vp8Packet[] getPackets()
{
return (Vp8Packet[])packets.toArray(new Vp8Packet[0]);
}
public void reset()
{
packets = new ArrayList();
}
private ArrayList packets;
}
package org.ifsoft.rtp;
import org.ifsoft.*;
import java.util.ArrayList;
import org.slf4j.*;
import org.slf4j.Logger;
public class Vp8Packet
{
private static final Logger Log = LoggerFactory.getLogger(Vp8Packet.class);
private Boolean _extendedControlBitsPresent;
private Byte _keyIndex;
private Boolean _keyIndexPresent;
private Boolean _layerSync;
public static Integer _maxPacketSize = Integer.valueOf(0);
private Boolean _nonReferenceFrame;
private Byte _partitionId;
private Byte _payload[];
private Short _pictureID;
private Boolean _pictureIDPresent;
private Boolean _startOfPartition;
private Byte _temporalLayerIndex;
private Boolean _temporalLayerIndexPresent;
private Byte _temporalLevelZeroIndex;
private Boolean _temporalLevelZeroIndexPresent;
static
{
_maxPacketSize = Integer.valueOf(1050);
}
public Vp8Packet()
{
_extendedControlBitsPresent = Boolean.valueOf(false);
_keyIndex = Byte.valueOf((byte)0);
_keyIndexPresent = Boolean.valueOf(false);
_layerSync = Boolean.valueOf(false);
_nonReferenceFrame = Boolean.valueOf(false);
_partitionId = Byte.valueOf((byte)0);
_pictureID = Short.valueOf((short)0);
_pictureIDPresent = Boolean.valueOf(false);
_startOfPartition = Boolean.valueOf(false);
_temporalLayerIndex = Byte.valueOf((byte)0);
_temporalLayerIndexPresent = Boolean.valueOf(false);
_temporalLevelZeroIndex = Byte.valueOf((byte)0);
_temporalLevelZeroIndexPresent = Boolean.valueOf(false);
}
public static Byte[] depacketize(Vp8Packet packets[])
{
Integer num = Integer.valueOf(0);
Vp8Packet arr[] = packets;
int len = arr.length;
for(int i = 0; i < len; i++)
{
Vp8Packet packet = arr[i];
num = Integer.valueOf(num.intValue() + ArrayExtensions.getLength(packet.getPayload()).intValue());
}
Integer destinationIndex = Integer.valueOf(0);
Byte destinationArray[] = new Byte[num.intValue()];
arr = packets;
len = arr.length;
for(int i = 0; i < len; i++)
{
Vp8Packet packet = arr[i];
ArrayExtensions.copy(packet.getPayload(), 0, destinationArray, destinationIndex.intValue(), ArrayExtensions.getLength(packet.getPayload()).intValue());
destinationIndex = Integer.valueOf(destinationIndex.intValue() + ArrayExtensions.getLength(packet.getPayload()).intValue());
}
return destinationArray;
}
public Byte[] getBytes()
{
ArrayList list = new ArrayList();
list.add(new Byte((new Byte((new Integer((getExtendedControlBitsPresent().booleanValue() ? 0x80 : 0) | (getNonReferenceFrame().booleanValue() ? 0x20 : 0) | (getStartOfPartition().booleanValue() ? 0x10 : 0) | getPartitionId().byteValue() & 0xf)).byteValue())).byteValue()));
if(getExtendedControlBitsPresent().booleanValue())
{
list.add(new Byte((new Byte((new Integer((getPictureIDPresent().booleanValue() ? 0x80 : 0) | (getTemporalLevelZeroIndexPresent().booleanValue() ? 0x40 : 0) | (getTemporalLayerIndexPresent().booleanValue() ? 0x20 : 0) | (getKeyIndexPresent().booleanValue() ? 0x10 : 0))).byteValue())).byteValue()));
if(getPictureIDPresent().booleanValue())
{
Byte shortBytesNetwork[] = BitAssistant.getShortBytesNetwork(getPictureID());
list.add(new Byte((new Byte((new Integer(0x80 | shortBytesNetwork[0].byteValue() & 0x7f)).byteValue())).byteValue()));
list.add(new Byte(shortBytesNetwork[1].byteValue()));
}
if(getTemporalLevelZeroIndexPresent().booleanValue())
list.add(new Byte(getTemporalLevelZeroIndex().byteValue()));
if(getTemporalLayerIndexPresent().booleanValue() || getKeyIndexPresent().booleanValue())
list.add(new Byte((new Byte((byte)(getTemporalLayerIndex().byteValue() << 6 & 0xc0 | (getLayerSync().booleanValue() ? 0x20 : 0) | getKeyIndex().byteValue() & 0x1f))).byteValue()));
}
ArrayListExtensions.addRange(list, getPayload());
return (Byte[])list.toArray(new Byte[0]);
}
public Boolean getExtendedControlBitsPresent()
{
return _extendedControlBitsPresent;
}
public Byte getKeyIndex()
{
return _keyIndex;
}
public Boolean getKeyIndexPresent()
{
return _keyIndexPresent;
}
public Boolean getLayerSync()
{
return _layerSync;
}
public Boolean getNonReferenceFrame()
{
return _nonReferenceFrame;
}
public Byte getPartitionId()
{
return _partitionId;
}
public Byte[] getPayload()
{
return _payload;
}
public Short getPictureID()
{
return _pictureID;
}
public Boolean getPictureIDPresent()
{
return _pictureIDPresent;
}
public Boolean getStartOfPartition()
{
return _startOfPartition;
}
public Byte getTemporalLayerIndex()
{
return _temporalLayerIndex;
}
public Boolean getTemporalLayerIndexPresent()
{
return _temporalLayerIndexPresent;
}
public Byte getTemporalLevelZeroIndex()
{
return _temporalLevelZeroIndex;
}
public Boolean getTemporalLevelZeroIndexPresent()
{
return _temporalLevelZeroIndexPresent;
}
public static Vp8Packet[] packetize(Byte encodedData[])
{
Integer offset = Integer.valueOf(0);
ArrayList list = new ArrayList();
Integer num2 = Integer.valueOf(_maxPacketSize.intValue() - 1);
Integer num3 = new Integer((new Double(org.ifsoft.Math.ceiling(new Double((new Double((new Integer(ArrayExtensions.getLength(encodedData).intValue())).doubleValue())).doubleValue() / (new Double((new Integer(num2.intValue())).doubleValue())).doubleValue())).doubleValue())).intValue());
Integer num4 = Integer.valueOf(ArrayExtensions.getLength(encodedData).intValue() / num3.intValue());
Integer num5 = Integer.valueOf(ArrayExtensions.getLength(encodedData).intValue() - num3.intValue() * num4.intValue());
for(Integer i = Integer.valueOf(0); i.intValue() < num3.intValue();)
{
Integer count = num4;
if(i.intValue() < num5.intValue())
{
Integer integer = count;
Integer integer1 = count = Integer.valueOf(count.intValue() + 1);
Integer _tmp = integer;
}
Vp8Packet item = new Vp8Packet();
item.setStartOfPartition(Boolean.valueOf(i.intValue() == 0));
item.setPayload(BitAssistant.subArray(encodedData, offset, count));
list.add(item);
offset = Integer.valueOf(offset.intValue() + count.intValue());
count = i;
i = Integer.valueOf(i.intValue() + 1);
Integer _tmp1 = count;
}
return (Vp8Packet[])list.toArray(new Vp8Packet[0]);
}
public static Vp8Packet parse(Byte packetBytes[])
{
Integer index = Integer.valueOf(0);
Vp8Packet packet = new Vp8Packet();
Byte num2 = packetBytes[index.intValue()];
packet.setExtendedControlBitsPresent(Boolean.valueOf((num2.byteValue() & 0x80) == 128));
packet.setNonReferenceFrame(Boolean.valueOf((num2.byteValue() & 0x20) == 32));
packet.setStartOfPartition(Boolean.valueOf((num2.byteValue() & 0x10) == 16));
packet.setPartitionId(new Byte((byte)(num2.byteValue() & 0xf)));
Integer integer = index;
Integer integer1 = index = Integer.valueOf(index.intValue() + 1);
Integer _tmp = integer;
if(packet.getExtendedControlBitsPresent().booleanValue())
{
Byte num3 = packetBytes[index.intValue()];
packet.setPictureIDPresent(Boolean.valueOf((num3.byteValue() & 0x80) == 128));
packet.setTemporalLevelZeroIndexPresent(Boolean.valueOf((num3.byteValue() & 0x40) == 64));
packet.setTemporalLayerIndexPresent(Boolean.valueOf((num3.byteValue() & 0x20) == 32));
packet.setKeyIndexPresent(Boolean.valueOf((num3.byteValue() & 0x10) == 16));
Integer integer2 = index;
Integer integer5 = index = Integer.valueOf(index.intValue() + 1);
Integer _tmp1 = integer2;
if(packet.getPictureIDPresent().booleanValue())
if((packetBytes[index.intValue()].byteValue() & 0x80) == 128)
{
Byte buffer[] = BitAssistant.subArray(packetBytes, index, Integer.valueOf(2));
buffer[0] = new Byte((byte)(buffer[0].byteValue() & 0x7f));
packet.setPictureID(BitAssistant.toShortNetwork(buffer, Integer.valueOf(0)));
index = Integer.valueOf(index.intValue() + 2);
} else
{
Byte buffer2[] = new Byte[2];
buffer2[1] = packetBytes[index.intValue()];
packet.setPictureID(BitAssistant.toShortNetwork(buffer2, Integer.valueOf(0)));
Integer integer6 = index;
Integer integer9 = index = Integer.valueOf(index.intValue() + 1);
Integer _tmp2 = integer6;
}
if(packet.getTemporalLevelZeroIndexPresent().booleanValue())
{
packet.setTemporalLevelZeroIndex(packetBytes[index.intValue()]);
Integer integer3 = index;
Integer integer7 = index = Integer.valueOf(index.intValue() + 1);
Integer _tmp3 = integer3;
}
if(packet.getTemporalLayerIndexPresent().booleanValue() || packet.getKeyIndexPresent().booleanValue())
{
packet.setTemporalLayerIndex(new Byte((byte)(packetBytes[index.intValue()].byteValue() >> 6 & 3)));
packet.setLayerSync(Boolean.valueOf((packetBytes[index.intValue()].byteValue() & 0x20) == 32));
packet.setKeyIndex(new Byte((byte)(packetBytes[index.intValue()].byteValue() & 0x1f)));
Integer integer4 = index;
Integer integer8 = index = Integer.valueOf(index.intValue() + 1);
Integer _tmp4 = integer4;
}
}
packet.setPayload(BitAssistant.subArray(packetBytes, index));
return packet;
}
private void setExtendedControlBitsPresent(Boolean value)
{
_extendedControlBitsPresent = value;
}
private void setKeyIndex(Byte value)
{
_keyIndex = value;
}
private void setKeyIndexPresent(Boolean value)
{
_keyIndexPresent = value;
}
private void setLayerSync(Boolean value)
{
_layerSync = value;
}
private void setNonReferenceFrame(Boolean value)
{
_nonReferenceFrame = value;
}
private void setPartitionId(Byte value)
{
_partitionId = value;
}
private void setPayload(Byte value[])
{
_payload = value;
}
private void setPictureID(Short value)
{
_pictureID = value;
}
private void setPictureIDPresent(Boolean value)
{
_pictureIDPresent = value;
}
private void setStartOfPartition(Boolean value)
{
_startOfPartition = value;
}
private void setTemporalLayerIndex(Byte value)
{
_temporalLayerIndex = value;
}
private void setTemporalLayerIndexPresent(Boolean value)
{
_temporalLayerIndexPresent = value;
}
private void setTemporalLevelZeroIndex(Byte value)
{
_temporalLevelZeroIndex = value;
}
private void setTemporalLevelZeroIndexPresent(Boolean value)
{
_temporalLevelZeroIndexPresent = value;
}
}
...@@ -968,6 +968,21 @@ public class PluginImpl implements Plugin, PropertyEventListener ...@@ -968,6 +968,21 @@ public class PluginImpl implements Plugin, PropertyEventListener
*/ */
public class Participant public class Participant
{ {
/**
*
*
*/
private Vp8Accumulator vp8Accumulator;
/**
*
*
*/
private Integer lastSequenceNumber;
/**
*
*
*/
private Boolean sequenceNumberingViolated;
/** /**
* *
* *
...@@ -1030,17 +1045,7 @@ public class PluginImpl implements Plugin, PropertyEventListener ...@@ -1030,17 +1045,7 @@ public class PluginImpl implements Plugin, PropertyEventListener
public boolean accept(DatagramPacket p) public boolean accept(DatagramPacket p)
{ {
byte[] data = p.getData(); byte[] data = p.getData();
recordVideo(data);
try{
RTPPacket packet = RTPPacket.parseBytes(BitAssistant.bytesToArray(data));
byte[] videoFrame = BitAssistant.bytesFromArray(packet.getPayload());
//Log.info("Video media " + " " + packet.getPayloadType() + " " + packet.getSequenceNumber() + " " + packet.getTimestamp() + " " + packet.getPayload());
if (recorder != null) recorder.write(videoFrame, 0, videoFrame.length);
} catch (Exception e) {
}
return true; return true;
} }
}; };
...@@ -1054,6 +1059,47 @@ public class PluginImpl implements Plugin, PropertyEventListener ...@@ -1054,6 +1059,47 @@ public class PluginImpl implements Plugin, PropertyEventListener
} }
} }
}; };
/**
*
*
*/
private void recordVideo(byte[] data)
{
Byte[] encodedFrame = null;
boolean isKeyframe = false;
try {
RTPPacket packet = RTPPacket.parseBytes(BitAssistant.bytesToArray(data));
if (packet != null)
{
Vp8Packet packet2 = Vp8Packet.parse(packet.getPayload());
if(packet2 == null) return;
vp8Accumulator.add(packet2);
if (packet.getMarker().booleanValue())
{
encodedFrame = Vp8Packet.depacketize(vp8Accumulator.getPackets());
isKeyframe = isKeyFrame(BitAssistant.bytesFromArray(encodedFrame)).booleanValue();
vp8Accumulator.reset();
}
if (recorder != null && encodedFrame != null)
{
Log.info("Video media " + " " + packet.getPayloadType() + " " + encodedFrame + " " + packet.getTimestamp() + " " + isKeyframe);
recorder.write(BitAssistant.bytesFromArray(encodedFrame), 0, encodedFrame.length, isKeyframe, packet.getTimestamp());
}
} else {
Log.error("recordVideo cannot parse packet data " + data);
}
} catch (Exception e) {
Log.error("Error writing video recording" , e);
}
}
/** /**
* *
* *
...@@ -1062,6 +1108,9 @@ public class PluginImpl implements Plugin, PropertyEventListener ...@@ -1062,6 +1108,9 @@ public class PluginImpl implements Plugin, PropertyEventListener
this.nickname = nickname; this.nickname = nickname;
this.user = user; this.user = user;
this.focusName = focusName; this.focusName = focusName;
this.sequenceNumberingViolated = Boolean.valueOf(false);
this.vp8Accumulator = new Vp8Accumulator();
this.lastSequenceNumber = Integer.valueOf(-1);
} }
/** /**
* *
...@@ -1105,10 +1154,10 @@ public class PluginImpl implements Plugin, PropertyEventListener ...@@ -1105,10 +1154,10 @@ public class PluginImpl implements Plugin, PropertyEventListener
mediaStream.addPropertyChangeListener(streamPropertyChangeListener); mediaStream.addPropertyChangeListener(streamPropertyChangeListener);
String recordingPath = JiveGlobals.getHomeDirectory() + File.separator + "resources" + File.separator + "spank" + File.separator + "rayo" + File.separator + "video_recordings"; String recordingPath = JiveGlobals.getHomeDirectory() + File.separator + "resources" + File.separator + "spank" + File.separator + "rayo" + File.separator + "video_recordings";
String fileName = "video-" + focusName + "-" + nickname + "-" + System.currentTimeMillis() + ".rtp"; String fileName = "video-" + focusName + "-" + nickname + "-" + System.currentTimeMillis() + ".webm";
try { try {
recorder = new Recorder(recordingPath, fileName, "rtp", false, 0, 0); recorder = new Recorder(recordingPath, fileName, "webm", false, 0, 0);
} catch (Exception e) { } catch (Exception e) {
Log.error("Error creating video recording " + fileName + " " + recordingPath, e); Log.error("Error creating video recording " + fileName + " " + recordingPath, e);
...@@ -1133,6 +1182,15 @@ public class PluginImpl implements Plugin, PropertyEventListener ...@@ -1133,6 +1182,15 @@ public class PluginImpl implements Plugin, PropertyEventListener
} }
} }
} }
/**
*
*
*/
private Boolean isKeyFrame(byte encodedFrame[])
{
return Boolean.valueOf(encodedFrame != null && encodedFrame.length > 0 && (encodedFrame[0] & 1) == 0);
}
} }
public class FocusAgent extends VirtualConnection public class FocusAgent extends VirtualConnection
...@@ -1759,7 +1817,7 @@ public class PluginImpl implements Plugin, PropertyEventListener ...@@ -1759,7 +1817,7 @@ public class PluginImpl implements Plugin, PropertyEventListener
byte[] ulawData = new byte[data.length /2]; byte[] ulawData = new byte[data.length /2];
AudioConversion.linearToUlaw(data, 0, ulawData, 0); AudioConversion.linearToUlaw(data, 0, ulawData, 0);
if (recorder != null) recorder.write(ulawData, 0, ulawData.length); if (recorder != null) recorder.write(ulawData, 0, ulawData.length, false, 0);
} catch (Exception e) { } catch (Exception e) {
......
...@@ -15,6 +15,11 @@ import java.io.FileNotFoundException; ...@@ -15,6 +15,11 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
import org.ebml.*;
import org.ebml.io.*;
import org.ebml.matroska.*;
import org.ebml.util.*;
import org.slf4j.*; import org.slf4j.*;
import org.slf4j.Logger; import org.slf4j.Logger;
...@@ -34,11 +39,16 @@ public class Recorder extends Thread ...@@ -34,11 +39,16 @@ public class Recorder extends Thread
private static String defaultRecordDirectory = "."; private static String defaultRecordDirectory = ".";
private String recordPath; private String recordPath;
private boolean recordRtp; private boolean recordRtp;
private boolean recordWebm;
private boolean recordAu;
private static String fileSeparator = System.getProperty("file.separator"); private static String fileSeparator = System.getProperty("file.separator");
private boolean done; private boolean done;
private boolean pcmu; private boolean pcmu;
private int sampleRate; private int sampleRate;
private int channels; private int channels;
private long lastTimecode = 0;
private MatroskaFileWriter mFW;
private FileDataWriter iFW;
public Recorder(String recordDirectory, String recordPath, String recordingType, boolean pcmu, int sampleRate, int channels) throws IOException public Recorder(String recordDirectory, String recordPath, String recordingType, boolean pcmu, int sampleRate, int channels) throws IOException
...@@ -50,11 +60,20 @@ public class Recorder extends Thread ...@@ -50,11 +60,20 @@ public class Recorder extends Thread
if (recordingType.equalsIgnoreCase("Rtp")) { if (recordingType.equalsIgnoreCase("Rtp")) {
recordRtp = true; recordRtp = true;
} else if (recordingType.equalsIgnoreCase("Au") == false) { openFile();
} else if (recordingType.equalsIgnoreCase("webm")) {
recordWebm = true;
openWebmFile();
} else if (recordingType.equalsIgnoreCase("Au")) {
recordAu = true;
openFile();
} else {
throw new IOException("Invalid recording type " + recordingType); throw new IOException("Invalid recording type " + recordingType);
} }
openFile();
start(); start();
} }
...@@ -90,11 +109,37 @@ public class Recorder extends Thread ...@@ -90,11 +109,37 @@ public class Recorder extends Thread
} }
private void openWebmFile() throws IOException
{
iFW = new FileDataWriter(recordPath);
mFW = new MatroskaFileWriter(iFW);
mFW.writeEBMLHeader();
mFW.writeSegmentHeader();
mFW.writeSegmentInfo();
MatroskaFileTrack track = new MatroskaFileTrack();
track.TrackNo = (short)1;
track.TrackUID = new java.util.Random().nextLong();
track.TrackType = MatroskaDocType.track_video;
track.Name = "VP8";
track.Language = "und";
track.CodecID = "V_VP8";
track.DefaultDuration = 0;
track.Video_PixelWidth = 640;
track.Video_PixelHeight = 360;
track.CodecPrivate = new byte[0];
mFW.TrackList.add(track);
mFW.writeTracks();
}
private void openFile() throws IOException { private void openFile() throws IOException {
File recordFile = new File(recordPath); File recordFile = new File(recordPath);
try { try {
synchronized(this) { synchronized(this)
{
if (recordFile.exists()) { if (recordFile.exists()) {
recordFile.delete(); recordFile.delete();
} }
...@@ -104,18 +149,16 @@ public class Recorder extends Thread ...@@ -104,18 +149,16 @@ public class Recorder extends Thread
fo = new FileOutputStream(recordFile); fo = new FileOutputStream(recordFile);
bo = new BufferedOutputStream(fo, BUFFER_SIZE); bo = new BufferedOutputStream(fo, BUFFER_SIZE);
if (recordRtp == false) { if (recordRtp)
writeAuHeader(); {
} else {
/*
* Write RTP header
*/
byte[] buf = new byte[16]; byte[] buf = new byte[16];
buf[0] = (byte) 0x52; // R buf[0] = (byte) 0x52; // R
buf[1] = (byte) 0x54; // T buf[1] = (byte) 0x54; // T
buf[2] = (byte) 0x50; // P buf[2] = (byte) 0x50; // P
bo.write(buf, 0, buf.length); bo.write(buf, 0, buf.length);
} else {
writeAuHeader();
} }
} }
} catch (IOException e) { } catch (IOException e) {
...@@ -254,8 +297,10 @@ public class Recorder extends Thread ...@@ -254,8 +297,10 @@ public class Recorder extends Thread
public byte[] data; public byte[] data;
public int offset; public int offset;
public int length; public int length;
public boolean keyframe;
public long timestamp;
public DataToWrite(byte[] data, int offset, int length) public DataToWrite(byte[] data, int offset, int length, boolean keyframe, long timestamp)
{ {
/* /*
* We have to copy the data, otherwise caller could * We have to copy the data, otherwise caller could
...@@ -264,6 +309,8 @@ public class Recorder extends Thread ...@@ -264,6 +309,8 @@ public class Recorder extends Thread
this.data = new byte[length]; this.data = new byte[length];
this.offset = offset; this.offset = offset;
this.length = length; this.length = length;
this.keyframe = keyframe;
this.timestamp = timestamp;
System.arraycopy(data, offset, this.data, 0, length); System.arraycopy(data, offset, this.data, 0, length);
} }
...@@ -271,7 +318,7 @@ public class Recorder extends Thread ...@@ -271,7 +318,7 @@ public class Recorder extends Thread
private long lastWriteTime; private long lastWriteTime;
public void writePacket(byte[] data, int offset, int dataLength) public void writePacket(byte[] data, int offset, int dataLength, boolean keyframe, long timestamp)
throws IOException { throws IOException {
if (recordRtp) { if (recordRtp) {
...@@ -295,13 +342,17 @@ public class Recorder extends Thread ...@@ -295,13 +342,17 @@ public class Recorder extends Thread
buf[3] = (byte) (timeChange & 0xff); buf[3] = (byte) (timeChange & 0xff);
System.arraycopy(data, offset, buf, 4, dataLength); System.arraycopy(data, offset, buf, 4, dataLength);
write(buf, 0, buf.length); write(buf, 0, buf.length, keyframe, timestamp);
} else if (recordWebm) {
} else { } else {
write(data, offset, dataLength); write(data, offset, dataLength, keyframe, timestamp);
} }
} }
public void write(int[] data, int offset, int length) throws IOException { public void write(int[] data, int offset, int length, boolean keyframe, long timestamp) throws IOException
{
byte[] byteData = new byte[length * 2]; byte[] byteData = new byte[length * 2];
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
...@@ -309,16 +360,16 @@ public class Recorder extends Thread ...@@ -309,16 +360,16 @@ public class Recorder extends Thread
byteData[(2 * i) + 1] = (byte) (data[i + offset] & 0xff); byteData[(2 * i) + 1] = (byte) (data[i + offset] & 0xff);
} }
write(byteData, 0, byteData.length); write(byteData, 0, byteData.length, keyframe, timestamp);
} }
public void write(byte[] data, int offset, int length) throws IOException { public void write(byte[] data, int offset, int length, boolean keyframe, long timestamp) throws IOException {
if (done) { if (done) {
return; return;
} }
synchronized(dataToWrite) { synchronized(dataToWrite) {
dataToWrite.add(new DataToWrite(data, offset, length)); dataToWrite.add(new DataToWrite(data, offset, length, keyframe, timestamp));
dataToWrite.notifyAll(); dataToWrite.notifyAll();
} }
} }
...@@ -367,10 +418,42 @@ public class Recorder extends Thread ...@@ -367,10 +418,42 @@ public class Recorder extends Thread
private void writeData(DataToWrite d) { private void writeData(DataToWrite d) {
try { try {
synchronized(this) { synchronized(this)
{
if (recordWebm)
{
long duration = 0;
if (d.keyframe || lastTimecode == 0)
{
if (lastTimecode > 0)
{
Log.info("writeData end cluster " + d.data);
duration = d.timestamp - lastTimecode;
mFW.endCluster();
}
Log.info("writeData start cluster " + d.timestamp);
mFW.startCluster(d.timestamp);
}
lastTimecode = d.timestamp;
MatroskaFileFrame frame = new MatroskaFileFrame();
frame.TrackNo = 1;
frame.Duration = duration;
frame.Timecode = d.timestamp;
frame.Reference = 0;
frame.KeyFrame = d.keyframe;
frame.Data = d.data;
mFW.addFrame(frame);
Log.info("writeData video " + d.data);
} else {
bo.write(d.data, 0, d.length); bo.write(d.data, 0, d.length);
dataSize += d.length; dataSize += d.length;
} }
}
} catch (IOException e) { } catch (IOException e) {
Log.error("Can't record to " + recordPath, e); Log.error("Can't record to " + recordPath, e);
done(); done();
...@@ -379,7 +462,14 @@ public class Recorder extends Thread ...@@ -379,7 +462,14 @@ public class Recorder extends Thread
private void writeDataSize() { private void writeDataSize() {
try { try {
synchronized(this) { synchronized(this)
{
if (recordWebm)
{
mFW.endCluster();
iFW.close();
} else {
if (bo != null) { if (bo != null) {
bo.flush(); bo.flush();
bo.close(); bo.close();
...@@ -407,6 +497,8 @@ public class Recorder extends Thread ...@@ -407,6 +497,8 @@ public class Recorder extends Thread
} }
} }
} }
}
} catch (IOException e) { } catch (IOException e) {
Log.error("Exception closing recording " + recordPath + " " + e.getMessage(), e); Log.error("Exception closing recording " + recordPath + " " + e.getMessage(), e);
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment