diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java index 4f880fcb27dd6d99b330ba99776e9248d38f98d6..b37abf05f512764a26f85dd7aedbcba90bade446 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java @@ -18,6 +18,9 @@ package org.bigbluebutton.api; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collection; @@ -29,11 +32,20 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.TreeMap; import java.util.Set; -import java.util.concurrent.*; - -import org.bigbluebutton.api.domain.*; +import java.util.TreeMap; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; + +import org.apache.http.client.utils.URIBuilder; +import org.bigbluebutton.api.domain.Meeting; +import org.bigbluebutton.api.domain.Recording; +import org.bigbluebutton.api.domain.User; +import org.bigbluebutton.api.domain.UserSession; import org.bigbluebutton.api.messaging.MessageListener; import org.bigbluebutton.api.messaging.RedisStorageService; import org.bigbluebutton.api.messaging.converters.messages.DestroyMeetingMessage; @@ -47,6 +59,8 @@ import org.bigbluebutton.api.messaging.messages.MeetingDestroyed; import org.bigbluebutton.api.messaging.messages.MeetingEnded; import org.bigbluebutton.api.messaging.messages.MeetingStarted; import org.bigbluebutton.api.messaging.messages.RegisterUser; +import org.bigbluebutton.api.messaging.messages.StunTurnInfoRequested; +import org.bigbluebutton.api.messaging.messages.UpdateRecordingStatus; import org.bigbluebutton.api.messaging.messages.UserJoined; import org.bigbluebutton.api.messaging.messages.UserJoinedVoice; import org.bigbluebutton.api.messaging.messages.UserLeft; @@ -60,7 +74,6 @@ import org.bigbluebutton.api2.IBbbWebApiGWApp; import org.bigbluebutton.common.messages.Constants; import org.bigbluebutton.common.messages.SendStunTurnInfoReplyMessage; import org.bigbluebutton.presentation.PresentationUrlDownloadService; -import org.bigbluebutton.api.messaging.messages.StunTurnInfoRequested; import org.bigbluebutton.web.services.RegisteredUserCleanupTimerTask; import org.bigbluebutton.web.services.callback.CallbackUrlService; import org.bigbluebutton.web.services.callback.MeetingEndedEvent; @@ -447,6 +460,14 @@ public class MeetingService implements MessageListener { message.meetingId, message.parentMeetingId); } } + + private void processUpdateRecordingStatus(UpdateRecordingStatus message) { + Meeting m = getMeeting(message.meetingId); + // Set only once + if (m != null && message.recording && !m.haveRecordingMarks()) { + m.setHaveRecordingMarks(message.recording); + } + } private void processEndBreakoutRoom(EndBreakoutRoom message) { processEndMeeting(new EndMeeting(message.breakoutMeetingId)); @@ -570,12 +591,18 @@ public class MeetingService implements MessageListener { Map<String, String> metadata = m.getMetadata(); if (metadata.containsKey(END_CALLBACK_URL)) { String callbackUrl = metadata.get(END_CALLBACK_URL); + try { + callbackUrl = new URIBuilder(new URI(callbackUrl)).addParameter("recordingmarks", m.haveRecordingMarks() ? "true" : "false").build().toURL().toString(); + } catch (MalformedURLException e) { + log.error("Malformed URL in callback url=[{}]", callbackUrl); + } catch (URISyntaxException e) { + log.error("URI Syntax error in callback url=[{}]", callbackUrl); + e.printStackTrace(); + } callbackUrlService.handleMessage(new MeetingEndedEvent(callbackUrl)); } processRemoveEndedMeeting(message); - - return; } } @@ -861,6 +888,8 @@ public class MeetingService implements MessageListener { processStunTurnInfoRequested((StunTurnInfoRequested) message); } else if (message instanceof CreateBreakoutRoom) { processCreateBreakoutRoom((CreateBreakoutRoom) message); + } else if (message instanceof UpdateRecordingStatus) { + processUpdateRecordingStatus((UpdateRecordingStatus) message); } } }; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java index 99ec4c9ed56522ed1fe982a65f304ac495c1e4d5..e3e1f66894877f6cc649b22b59d58375ffc81564 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java @@ -51,6 +51,7 @@ public class Meeting { private boolean record; private boolean autoStartRecording = false; private boolean allowStartStopRecording = false; + private boolean haveRecordingMarks = false; private boolean webcamsOnlyForModerator = false; private String dialNumber; private String defaultAvatarURL; @@ -212,7 +213,16 @@ public class Meeting { public Boolean isBreakout() { return isBreakout; } + + + public void setHaveRecordingMarks(boolean marks) { + haveRecordingMarks = marks; + } + public boolean haveRecordingMarks() { + return haveRecordingMarks; + } + public String getName() { return name; } @@ -495,7 +505,7 @@ public class Meeting { this.allowStartStopRecording = allow; return this; } - + public Builder withWebcamsOnlyForModerator(boolean only) { this.webcamsOnlyForModerator = only; return this; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/UpdateRecordingStatus.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/UpdateRecordingStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..ba77958dccd430ab4f8f3677c7a7888b6460fec3 --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/UpdateRecordingStatus.java @@ -0,0 +1,11 @@ +package org.bigbluebutton.api.messaging.messages; + +public class UpdateRecordingStatus implements IMessage { + public final String meetingId; + public final Boolean recording; + + public UpdateRecordingStatus(String meetingId, Boolean recording) { + this.meetingId = meetingId; + this.recording = recording; + } +} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/handlers/AddNamespaceToSvgHandler.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/handlers/AddNamespaceToSvgHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..44fe2965a8512a6bc12be4129c5d5b731e4e52f1 --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/handlers/AddNamespaceToSvgHandler.java @@ -0,0 +1,9 @@ +package org.bigbluebutton.presentation.handlers; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AddNamespaceToSvgHandler extends AbstractCommandHandler { + private static Logger log = LoggerFactory.getLogger(AddNamespaceToSvgHandler.class); + +} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/handlers/Png2SvgConversionHandler.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/handlers/Png2SvgConversionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..90c087baf0ad5958946dd9656d70816339db8bde --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/handlers/Png2SvgConversionHandler.java @@ -0,0 +1,8 @@ +package org.bigbluebutton.presentation.handlers; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Png2SvgConversionHandler extends AbstractCommandHandler { + private static Logger log = LoggerFactory.getLogger(Png2SvgConversionHandler.class); +} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/handlers/SvgConversionHandler.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/handlers/SvgConversionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..3403aa8433736a139e601820d12edb0c59b0351a --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/handlers/SvgConversionHandler.java @@ -0,0 +1,54 @@ +package org.bigbluebutton.presentation.handlers; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SvgConversionHandler extends AbstractCommandHandler { + private static Logger log = LoggerFactory.getLogger(SvgConversionHandler.class); + + private static String PATH_OUTPUT = "<path"; + private static String IMAGE_TAG_OUTPUT = "<image"; + private static String PATH_PATTERN = "\\d+\\s" + PATH_OUTPUT; + private static String IMAGE_TAG_PATTERN = "\\d+\\s" + IMAGE_TAG_OUTPUT; + + /** + * + * @return The number of <path/> tags in the generated SVG + */ + public int numberOfPaths() { + if (stdoutContains(PATH_OUTPUT)) { + try { + String out = stdoutBuilder.toString(); + Pattern r = Pattern.compile(PATH_PATTERN); + Matcher m = r.matcher(out); + m.find(); + return Integer.parseInt(m.group(0).replace(PATH_OUTPUT, "").trim()); + } catch (Exception e) { + return 0; + } + } + return 0; + } + + /** + * + * @return The number of <image/> tags in the generated SVG. + */ + public int numberOfImageTags() { + if (stdoutContains(IMAGE_TAG_OUTPUT)) { + try { + String out = stdoutBuilder.toString(); + Pattern r = Pattern.compile(IMAGE_TAG_PATTERN); + Matcher m = r.matcher(out); + m.find(); + return Integer.parseInt(m.group(0).replace(IMAGE_TAG_OUTPUT, "").trim()); + } catch (Exception e) { + return 0; + } + } + return 0; + } +} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/SvgImageCreatorImp.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/SvgImageCreatorImp.java index 57094bc8e746adafbf25dbc07f838f2533eb3a58..939c481c07b75b38463dc0e157fff7f1969aed60 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/SvgImageCreatorImp.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/SvgImageCreatorImp.java @@ -1,109 +1,231 @@ package org.bigbluebutton.presentation.imp; import java.io.File; +import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.TimeUnit; -import com.google.gson.Gson; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; import org.bigbluebutton.presentation.SupportedFileTypes; import org.bigbluebutton.presentation.SvgImageCreator; import org.bigbluebutton.presentation.UploadedPresentation; +import org.bigbluebutton.presentation.handlers.AddNamespaceToSvgHandler; +import org.bigbluebutton.presentation.handlers.Pdf2PngPageConverterHandler; +import org.bigbluebutton.presentation.handlers.Png2SvgConversionHandler; +import org.bigbluebutton.presentation.handlers.SvgConversionHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class SvgImageCreatorImp implements SvgImageCreator { - private static Logger log = LoggerFactory.getLogger(SvgImageCreatorImp.class); - - private String IMAGEMAGICK_DIR; - - @Override - public boolean createSvgImages(UploadedPresentation pres) { - boolean success = false; - File svgImagesPresentationDir = determineSvgImagesDirectory( - pres.getUploadedFile()); - if (!svgImagesPresentationDir.exists()) - svgImagesPresentationDir.mkdir(); +import com.google.gson.Gson; +import com.zaxxer.nuprocess.NuProcess; +import com.zaxxer.nuprocess.NuProcessBuilder; - cleanDirectory(svgImagesPresentationDir); +public class SvgImageCreatorImp implements SvgImageCreator { + private static Logger log = LoggerFactory.getLogger(SvgImageCreatorImp.class); + + private long imageTagThreshold; + private long pathsThreshold; + private String convTimeout = "7s"; + private int WAIT_FOR_SEC = 7; + + @Override + public boolean createSvgImages(UploadedPresentation pres) { + boolean success = false; + File svgImagesPresentationDir = determineSvgImagesDirectory(pres.getUploadedFile()); + if (!svgImagesPresentationDir.exists()) + svgImagesPresentationDir.mkdir(); + + try { + FileUtils.cleanDirectory(svgImagesPresentationDir); + success = generateSvgImages(svgImagesPresentationDir, pres); + } catch (Exception e) { + log.warn("Interrupted Exception while generating images."); + success = false; + } - try { - success = generateSvgImages(svgImagesPresentationDir, pres); - } catch (InterruptedException e) { - log.warn("Interrupted Exception while generating images."); - success = false; + return success; } - return success; - } - - private boolean generateSvgImages(File imagePresentationDir, - UploadedPresentation pres) throws InterruptedException { - String source = pres.getUploadedFile().getAbsolutePath(); - String dest; - String COMMAND = ""; - boolean done = true; - if (SupportedFileTypes.isImageFile(pres.getFileType())) { - dest = imagePresentationDir.getAbsolutePath() + File.separator - + "slide1.pdf"; - COMMAND = IMAGEMAGICK_DIR + File.separator + "convert " + source + " " - + dest; - - done = new ExternalProcessExecutor().exec(COMMAND, 60000); - - source = imagePresentationDir.getAbsolutePath() + File.separator - + "slide1.pdf"; - dest = imagePresentationDir.getAbsolutePath() + File.separator - + "slide1.svg"; - COMMAND = "pdftocairo -rx 300 -ry 300 -svg -q -f 1 -l 1 " + source + " " - + dest; - - done = new ExternalProcessExecutor().exec(COMMAND, 60000); - - } else { - for (int i = 1; i <= pres.getNumberOfPages(); i++) { - File destsvg = new File(imagePresentationDir.getAbsolutePath() - + File.separatorChar + "slide" + i + ".svg"); - COMMAND = "pdftocairo -rx 300 -ry 300 -svg -q -f " + i + " -l " + i - + " " + File.separatorChar + source + " " - + destsvg.getAbsolutePath(); - - done = new ExternalProcessExecutor().exec(COMMAND, 60000); - if (!done) { - break; + private boolean generateSvgImages(File imagePresentationDir, UploadedPresentation pres) + throws InterruptedException { + String source = pres.getUploadedFile().getAbsolutePath(); + String dest; + int numSlides; + boolean done = true; + if (SupportedFileTypes.isImageFile(pres.getFileType())) { + numSlides = 1; + dest = imagePresentationDir.getAbsolutePath() + File.separator + "slide1.pdf"; + + NuProcessBuilder convertImgToSvg = new NuProcessBuilder(Arrays.asList("convert " + source + " " + dest)); + + NuProcess process = convertImgToSvg.start(); + try { + process.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS); + done = true; + } catch (InterruptedException e) { + done = false; + log.error(e.getMessage()); + } + + source = imagePresentationDir.getAbsolutePath() + File.separator + "slide1.pdf"; + } else { + numSlides = pres.getNumberOfPages(); } - } - } + for (int i = 1; i <= numSlides; i++) { + int numPages = 0; // total numbers of this SVG + + File destsvg = new File(imagePresentationDir.getAbsolutePath() + File.separatorChar + "slide" + i + ".svg"); + + NuProcessBuilder convertPdfToSvg = createConversionProcess("-svg", i, source, destsvg.getAbsolutePath(), + true); + + SvgConversionHandler pHandler = new SvgConversionHandler(); + convertPdfToSvg.setProcessListener(pHandler); + + NuProcess process = convertPdfToSvg.start(); + try { + process.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS); + done = true; + } catch (InterruptedException e) { + log.error(e.getMessage()); + } + if (!done) { + break; + } + + if (destsvg.length() == 0 || pHandler.numberOfImageTags() > imageTagThreshold + || pHandler.numberOfPaths() > pathsThreshold) { + // We need t delete the destination file as we are starting a + // new conversion process + if (destsvg.exists()) { + destsvg.delete(); + } + + done = false; + + Map<String, Object> logData = new HashMap<String, Object>(); + logData.put("meetingId", pres.getMeetingId()); + logData.put("presId", pres.getId()); + logData.put("filename", pres.getName()); + logData.put("page", i); + logData.put("convertSuccess", pHandler.isCommandSuccessful()); + logData.put("fileExists", destsvg.exists()); + logData.put("numberOfImages", pHandler.numberOfImageTags()); + logData.put("numberOfPaths", pHandler.numberOfPaths()); + logData.put("message", "Potential problem with generated SVG"); + Gson gson = new Gson(); + String logStr = gson.toJson(logData); + + log.warn("-- analytics -- {}", logStr); + + File tempPng = null; + String basePresentationame = FilenameUtils.getBaseName(pres.getName()); + try { + tempPng = File.createTempFile(basePresentationame + "-" + i, ".png"); + } catch (IOException ioException) { + // We should never fall into this if the server is correctly + // configured + logData = new HashMap<String, Object>(); + logData.put("meetingId", pres.getMeetingId()); + logData.put("presId", pres.getId()); + logData.put("filename", pres.getName()); + logData.put("message", "Unable to create temporary files"); + gson = new Gson(); + logStr = gson.toJson(logData); + log.error("-- analytics -- {}", logStr); + } + + // Step 1: Convert a PDF page to PNG using a raw pdftocairo + NuProcessBuilder convertPdfToPng = createConversionProcess("-png", i, source, + tempPng.getAbsolutePath().substring(0, tempPng.getAbsolutePath().lastIndexOf('.')), false); + + Pdf2PngPageConverterHandler pngHandler = new Pdf2PngPageConverterHandler(); + convertPdfToPng.setProcessListener(pngHandler); + NuProcess pngProcess = convertPdfToPng.start(); + try { + pngProcess.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.error(e.getMessage()); + } + + // Step 2: Convert a PNG image to SVG + NuProcessBuilder convertPngToSvg = new NuProcessBuilder(Arrays.asList("timeout", convTimeout, "convert", + tempPng.getAbsolutePath(), destsvg.getAbsolutePath())); + + Png2SvgConversionHandler svgHandler = new Png2SvgConversionHandler(); + convertPngToSvg.setProcessListener(svgHandler); + NuProcess svgProcess = convertPngToSvg.start(); + try { + svgProcess.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.error(e.getMessage()); + } + + done = svgHandler.isCommandSuccessful(); + + // Delete the temporary PNG after finishing the image conversion + tempPng.delete(); + + // Step 3: Add SVG namespace to the destionation file + // Check : https://phabricator.wikimedia.org/T43174 + NuProcessBuilder addNameSpaceToSVG = new NuProcessBuilder(Arrays.asList("timeout", convTimeout, + "/bin/sh", "-c", + "sed -i " + + "'4s|>| xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.2\">|' " + + destsvg.getAbsolutePath())); + + AddNamespaceToSvgHandler namespaceHandler = new AddNamespaceToSvgHandler(); + addNameSpaceToSVG.setProcessListener(namespaceHandler); + NuProcess namespaceProcess = addNameSpaceToSVG.start(); + try { + namespaceProcess.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.error(e.getMessage()); + } + } - if (done) { - return true; - } + } + + if (done) { + return true; + } - Map<String, Object> logData = new HashMap<String, Object>(); - logData.put("meetingId", pres.getMeetingId()); - logData.put("presId", pres.getId()); - logData.put("filename", pres.getName()); - logData.put("message", "Failed to create svg images."); + Map<String, Object> logData = new HashMap<String, Object>(); + logData.put("meetingId", pres.getMeetingId()); + logData.put("presId", pres.getId()); + logData.put("filename", pres.getName()); + logData.put("message", "Failed to create svg images."); - Gson gson = new Gson(); - String logStr = gson.toJson(logData); - log.warn("-- analytics -- " + logStr); + Gson gson = new Gson(); + String logStr = gson.toJson(logData); + log.warn("-- analytics -- " + logStr); - return false; - } + return false; + } - private File determineSvgImagesDirectory(File presentationFile) { - return new File(presentationFile.getParent() + File.separatorChar + "svgs"); - } + private NuProcessBuilder createConversionProcess(String format, int page, String source, String destFile, + boolean analyze) { + String rawCommand = "pdftocairo -r " + (analyze ? " 300 " : " 150 ") + format + (analyze ? "" : " -singlefile") + + " -q -f " + String.valueOf(page) + " -l " + String.valueOf(page) + " " + source + " " + destFile; + if (analyze) { + rawCommand += " && cat " + destFile; + rawCommand += " | egrep 'data:image/png;base64|<path' | sed 's/ / /g' | cut -d' ' -f 1 | sort | uniq -cw 2"; + } + return new NuProcessBuilder(Arrays.asList("timeout", convTimeout, "/bin/sh", "-c", rawCommand)); + } - private void cleanDirectory(File directory) { - File[] files = directory.listFiles(); - for (int i = 0; i < files.length; i++) { - files[i].delete(); + private File determineSvgImagesDirectory(File presentationFile) { + return new File(presentationFile.getParent() + File.separatorChar + "svgs"); } - } - public void setImageMagickDir(String imageMagickDir) { - IMAGEMAGICK_DIR = imageMagickDir; - } + public void setImageTagThreshold(long threshold) { + imageTagThreshold = threshold; + } + public void setPathsThreshold(long threshold) { + pathsThreshold = threshold; + } } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/web/services/callback/CallbackUrlService.java b/bbb-common-web/src/main/java/org/bigbluebutton/web/services/callback/CallbackUrlService.java index 8044f00e88ba466363fc7d527e9e5a091c74f60a..c06145228676ab423b86508f068988d5d05b9865 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/web/services/callback/CallbackUrlService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/web/services/callback/CallbackUrlService.java @@ -1,24 +1,22 @@ package org.bigbluebutton.web.services.callback; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; + import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.HttpGet; -import org.apache.http.entity.ContentType; import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; import org.apache.http.impl.nio.client.HttpAsyncClients; -import org.apache.http.nio.client.methods.HttpAsyncMethods; -import org.apache.http.nio.client.methods.ZeroCopyConsumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.concurrent.*; - public class CallbackUrlService { private static Logger log = LoggerFactory.getLogger(CallbackUrlService.class); @@ -119,10 +117,10 @@ public class CallbackUrlService { } private boolean fetchCallbackUrl(final String callbackUrl) { - log.info("Calling callback url {}", callbackUrl); - String finalUrl = followRedirect(callbackUrl, 0, callbackUrl); + log.info("Calling callback url {}", finalUrl); + if (finalUrl == null) return false; boolean success = false; diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/bus/ReceivedJsonMsgHdlrActor.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/bus/ReceivedJsonMsgHdlrActor.scala index fdac5722337d9ed07af247fa19b1fd21e6003420..700e83a626b9e1451b22ef6d9110459e4b9796f7 100755 --- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/bus/ReceivedJsonMsgHdlrActor.scala +++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/bus/ReceivedJsonMsgHdlrActor.scala @@ -87,6 +87,8 @@ class ReceivedJsonMsgHdlrActor(val msgFromAkkaAppsEventBus: MsgFromAkkaAppsEvent route[UserRoleChangedEvtMsg](envelope, jsonNode) case CreateBreakoutRoomSysCmdMsg.NAME => route[CreateBreakoutRoomSysCmdMsg](envelope, jsonNode) + case RecordingStatusChangedEvtMsg.NAME => + route[RecordingStatusChangedEvtMsg](envelope, jsonNode) case _ => //log.debug("************ Cannot route envelope name " + envelope.name) diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala index dfc45af7d8c4ded5365c7fae5563bfded515b78a..b034d89b228bdc14f15d2c88dac5a2f0c6ad8694 100755 --- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala +++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala @@ -36,6 +36,7 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW) case m: UserBroadcastCamStartedEvtMsg => handleUserBroadcastCamStartedEvtMsg(m) case m: UserBroadcastCamStoppedEvtMsg => handleUserBroadcastCamStoppedEvtMsg(m) case m: CreateBreakoutRoomSysCmdMsg => handleCreateBreakoutRoomSysCmdMsg(m) + case m: RecordingStatusChangedEvtMsg => handleRecordingStatusChangedEvtMsg(m) case _ => log.error("***** Cannot handle " + msg.envelope.name) } } @@ -69,6 +70,10 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW) )) } + + def handleRecordingStatusChangedEvtMsg(msg: RecordingStatusChangedEvtMsg): Unit = { + olgMsgGW.handle(new UpdateRecordingStatus(msg.header.meetingId, msg.body.recording)); + } def handleCheckAlivePongSysMsg(msg: CheckAlivePongSysMsg): Unit = { olgMsgGW.handle(new org.bigbluebutton.api.messaging.messages.KeepAliveReply(msg.body.system, msg.body.timestamp)) diff --git a/bigbluebutton-client/resources/prod/lib/adapter.js b/bigbluebutton-client/resources/prod/lib/adapter.js index ce68e660b587706d740dd6fb54573a8d19071ab7..ac7d7a5acebd1b2300d557f59f14e009efea3529 100755 --- a/bigbluebutton-client/resources/prod/lib/adapter.js +++ b/bigbluebutton-client/resources/prod/lib/adapter.js @@ -1,4 +1,4 @@ -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.adapter = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.adapter = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ /* * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. * @@ -11,16 +11,6 @@ var SDPUtils = require('sdp'); -function fixStatsType(stat) { - return { - inboundrtp: 'inbound-rtp', - outboundrtp: 'outbound-rtp', - candidatepair: 'candidate-pair', - localcandidate: 'local-candidate', - remotecandidate: 'remote-candidate' - }[stat.type] || stat.type; -} - function writeMediaSection(transceiver, caps, type, stream, dtlsRole) { var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps); @@ -35,7 +25,9 @@ function writeMediaSection(transceiver, caps, type, stream, dtlsRole) { sdp += 'a=mid:' + transceiver.mid + '\r\n'; - if (transceiver.rtpSender && transceiver.rtpReceiver) { + if (transceiver.direction) { + sdp += 'a=' + transceiver.direction + '\r\n'; + } else if (transceiver.rtpSender && transceiver.rtpReceiver) { sdp += 'a=sendrecv\r\n'; } else if (transceiver.rtpSender) { sdp += 'a=sendonly\r\n'; @@ -46,18 +38,14 @@ function writeMediaSection(transceiver, caps, type, stream, dtlsRole) { } if (transceiver.rtpSender) { - var trackId = transceiver.rtpSender._initialTrackId || - transceiver.rtpSender.track.id; - transceiver.rtpSender._initialTrackId = trackId; // spec. - var msid = 'msid:' + (stream ? stream.id : '-') + ' ' + - trackId + '\r\n'; + var msid = 'msid:' + stream.id + ' ' + + transceiver.rtpSender.track.id + '\r\n'; sdp += 'a=' + msid; - // for Chrome. Legacy should no longer be required. + + // for Chrome. sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + ' ' + msid; - - // RTX if (transceiver.sendEncodingParameters[0].rtx) { sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc + ' ' + msid; @@ -113,6 +101,7 @@ function filterIceServers(iceServers, edgeVersion) { server.urls = isString ? urls[0] : urls; return !!urls.length; } + return false; }); } @@ -226,57 +215,25 @@ function maybeAddCandidate(iceTransport, candidate) { return !alreadyAdded; } - -function makeError(name, description) { - var e = new Error(description); - e.name = name; - // legacy error codes from https://heycam.github.io/webidl/#idl-DOMException-error-names - e.code = { - NotSupportedError: 9, - InvalidStateError: 11, - InvalidAccessError: 15, - TypeError: undefined, - OperationError: undefined - }[name]; - return e; -} - module.exports = function(window, edgeVersion) { - // https://w3c.github.io/mediacapture-main/#mediastream - // Helper function to add the track to the stream and - // dispatch the event ourselves. - function addTrackToStreamAndFireEvent(track, stream) { - stream.addTrack(track); - stream.dispatchEvent(new window.MediaStreamTrackEvent('addtrack', - {track: track})); - } - - function removeTrackFromStreamAndFireEvent(track, stream) { - stream.removeTrack(track); - stream.dispatchEvent(new window.MediaStreamTrackEvent('removetrack', - {track: track})); - } - - function fireAddTrack(pc, track, receiver, streams) { - var trackEvent = new Event('track'); - trackEvent.track = track; - trackEvent.receiver = receiver; - trackEvent.transceiver = {receiver: receiver}; - trackEvent.streams = streams; - window.setTimeout(function() { - pc._dispatchEvent('track', trackEvent); - }); - } - var RTCPeerConnection = function(config) { - var pc = this; + var self = this; var _eventTarget = document.createDocumentFragment(); ['addEventListener', 'removeEventListener', 'dispatchEvent'] .forEach(function(method) { - pc[method] = _eventTarget[method].bind(_eventTarget); + self[method] = _eventTarget[method].bind(_eventTarget); }); + this.onicecandidate = null; + this.onaddstream = null; + this.ontrack = null; + this.onremovestream = null; + this.onsignalingstatechange = null; + this.oniceconnectionstatechange = null; + this.onicegatheringstatechange = null; + this.onnegotiationneeded = null; + this.ondatachannel = null; this.canTrickleIceCandidates = null; this.needNegotiation = false; @@ -284,20 +241,20 @@ module.exports = function(window, edgeVersion) { this.localStreams = []; this.remoteStreams = []; - this._localDescription = null; - this._remoteDescription = null; + this.localDescription = null; + this.remoteDescription = null; this.signalingState = 'stable'; this.iceConnectionState = 'new'; - this.connectionState = 'new'; this.iceGatheringState = 'new'; config = JSON.parse(JSON.stringify(config || {})); this.usingBundle = config.bundlePolicy === 'max-bundle'; if (config.rtcpMuxPolicy === 'negotiate') { - throw(makeError('NotSupportedError', - 'rtcpMuxPolicy \'negotiate\' is not supported')); + var e = new Error('rtcpMuxPolicy \'negotiate\' is not supported'); + e.name = 'NotSupportedError'; + throw(e); } else if (!config.rtcpMuxPolicy) { config.rtcpMuxPolicy = 'require'; } @@ -326,10 +283,10 @@ module.exports = function(window, edgeVersion) { this._iceGatherers = []; if (config.iceCandidatePoolSize) { for (var i = config.iceCandidatePoolSize; i > 0; i--) { - this._iceGatherers.push(new window.RTCIceGatherer({ + this._iceGatherers = new window.RTCIceGatherer({ iceServers: config.iceServers, gatherPolicy: config.iceTransportPolicy - })); + }); } } else { config.iceCandidatePoolSize = 0; @@ -345,48 +302,14 @@ module.exports = function(window, edgeVersion) { this._sdpSessionVersion = 0; this._dtlsRole = undefined; // role for a=setup to use in answers. - - this._isClosed = false; - }; - - Object.defineProperty(RTCPeerConnection.prototype, 'localDescription', { - configurable: true, - get: function() { - return this._localDescription; - } - }); - Object.defineProperty(RTCPeerConnection.prototype, 'remoteDescription', { - configurable: true, - get: function() { - return this._remoteDescription; - } - }); - - // set up event handlers on prototype - RTCPeerConnection.prototype.onicecandidate = null; - RTCPeerConnection.prototype.onaddstream = null; - RTCPeerConnection.prototype.ontrack = null; - RTCPeerConnection.prototype.onremovestream = null; - RTCPeerConnection.prototype.onsignalingstatechange = null; - RTCPeerConnection.prototype.oniceconnectionstatechange = null; - RTCPeerConnection.prototype.onconnectionstatechange = null; - RTCPeerConnection.prototype.onicegatheringstatechange = null; - RTCPeerConnection.prototype.onnegotiationneeded = null; - RTCPeerConnection.prototype.ondatachannel = null; - - RTCPeerConnection.prototype._dispatchEvent = function(name, event) { - if (this._isClosed) { - return; - } - this.dispatchEvent(event); - if (typeof this['on' + name] === 'function') { - this['on' + name](event); - } }; RTCPeerConnection.prototype._emitGatheringStateChange = function() { var event = new Event('icegatheringstatechange'); - this._dispatchEvent('icegatheringstatechange', event); + this.dispatchEvent(event); + if (typeof this.onicegatheringstatechange === 'function') { + this.onicegatheringstatechange(event); + } }; RTCPeerConnection.prototype.getConfiguration = function() { @@ -402,8 +325,8 @@ module.exports = function(window, edgeVersion) { }; // internal helper to create a transceiver object. - // (which is not yet the same as the WebRTC 1.0 transceiver) - RTCPeerConnection.prototype._createTransceiver = function(kind, doNotAdd) { + // (whih is not yet the same as the WebRTC 1.0 transceiver) + RTCPeerConnection.prototype._createTransceiver = function(kind) { var hasBundleTransport = this.transceivers.length > 0; var transceiver = { track: null, @@ -419,7 +342,6 @@ module.exports = function(window, edgeVersion) { sendEncodingParameters: null, recvEncodingParameters: null, stream: null, - associatedRemoteMediaStreams: [], wantReceive: true }; if (this.usingBundle && hasBundleTransport) { @@ -430,26 +352,11 @@ module.exports = function(window, edgeVersion) { transceiver.iceTransport = transports.iceTransport; transceiver.dtlsTransport = transports.dtlsTransport; } - if (!doNotAdd) { - this.transceivers.push(transceiver); - } + this.transceivers.push(transceiver); return transceiver; }; RTCPeerConnection.prototype.addTrack = function(track, stream) { - if (this._isClosed) { - throw makeError('InvalidStateError', - 'Attempted to call addTrack on a closed peerconnection.'); - } - - var alreadyExists = this.transceivers.find(function(s) { - return s.track === track; - }); - - if (alreadyExists) { - throw makeError('InvalidAccessError', 'Track already exists.'); - } - var transceiver; for (var i = 0; i < this.transceivers.length; i++) { if (!this.transceivers[i].track && @@ -475,10 +382,10 @@ module.exports = function(window, edgeVersion) { }; RTCPeerConnection.prototype.addStream = function(stream) { - var pc = this; + var self = this; if (edgeVersion >= 15025) { stream.getTracks().forEach(function(track) { - pc.addTrack(track, stream); + self.addTrack(track, stream); }); } else { // Clone is necessary for local demos mostly, attaching directly @@ -492,59 +399,17 @@ module.exports = function(window, edgeVersion) { }); }); clonedStream.getTracks().forEach(function(track) { - pc.addTrack(track, clonedStream); + self.addTrack(track, clonedStream); }); } }; - RTCPeerConnection.prototype.removeTrack = function(sender) { - if (this._isClosed) { - throw makeError('InvalidStateError', - 'Attempted to call removeTrack on a closed peerconnection.'); - } - - if (!(sender instanceof window.RTCRtpSender)) { - throw new TypeError('Argument 1 of RTCPeerConnection.removeTrack ' + - 'does not implement interface RTCRtpSender.'); - } - - var transceiver = this.transceivers.find(function(t) { - return t.rtpSender === sender; - }); - - if (!transceiver) { - throw makeError('InvalidAccessError', - 'Sender was not created by this connection.'); - } - var stream = transceiver.stream; - - transceiver.rtpSender.stop(); - transceiver.rtpSender = null; - transceiver.track = null; - transceiver.stream = null; - - // remove the stream from the set of local streams - var localStreams = this.transceivers.map(function(t) { - return t.stream; - }); - if (localStreams.indexOf(stream) === -1 && - this.localStreams.indexOf(stream) > -1) { - this.localStreams.splice(this.localStreams.indexOf(stream), 1); - } - - this._maybeFireNegotiationNeeded(); - }; - RTCPeerConnection.prototype.removeStream = function(stream) { - var pc = this; - stream.getTracks().forEach(function(track) { - var sender = pc.getSenders().find(function(s) { - return s.track === track; - }); - if (sender) { - pc.removeTrack(sender); - } - }); + var idx = this.localStreams.indexOf(stream); + if (idx > -1) { + this.localStreams.splice(idx, 1); + this._maybeFireNegotiationNeeded(); + } }; RTCPeerConnection.prototype.getSenders = function() { @@ -568,7 +433,7 @@ module.exports = function(window, edgeVersion) { RTCPeerConnection.prototype._createIceGatherer = function(sdpMLineIndex, usingBundle) { - var pc = this; + var self = this; if (usingBundle && sdpMLineIndex > 0) { return this.transceivers[0].iceGatherer; } else if (this._iceGatherers.length) { @@ -582,14 +447,14 @@ module.exports = function(window, edgeVersion) { {value: 'new', writable: true} ); - this.transceivers[sdpMLineIndex].bufferedCandidateEvents = []; + this.transceivers[sdpMLineIndex].candidates = []; this.transceivers[sdpMLineIndex].bufferCandidates = function(event) { var end = !event.candidate || Object.keys(event.candidate).length === 0; // polyfill since RTCIceGatherer.state is not implemented in // Edge 10547 yet. iceGatherer.state = end ? 'completed' : 'gathering'; - if (pc.transceivers[sdpMLineIndex].bufferedCandidateEvents !== null) { - pc.transceivers[sdpMLineIndex].bufferedCandidateEvents.push(event); + if (self.transceivers[sdpMLineIndex].candidates !== null) { + self.transceivers[sdpMLineIndex].candidates.push(event.candidate); } }; iceGatherer.addEventListener('localcandidate', @@ -599,18 +464,17 @@ module.exports = function(window, edgeVersion) { // start gathering from an RTCIceGatherer. RTCPeerConnection.prototype._gather = function(mid, sdpMLineIndex) { - var pc = this; + var self = this; var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer; if (iceGatherer.onlocalcandidate) { return; } - var bufferedCandidateEvents = - this.transceivers[sdpMLineIndex].bufferedCandidateEvents; - this.transceivers[sdpMLineIndex].bufferedCandidateEvents = null; + var candidates = this.transceivers[sdpMLineIndex].candidates; + this.transceivers[sdpMLineIndex].candidates = null; iceGatherer.removeEventListener('localcandidate', this.transceivers[sdpMLineIndex].bufferCandidates); iceGatherer.onlocalcandidate = function(evt) { - if (pc.usingBundle && sdpMLineIndex > 0) { + if (self.usingBundle && sdpMLineIndex > 0) { // if we know that we use bundle we can drop candidates with // Ñ•dpMLineIndex > 0. If we don't do this then our state gets // confused since we dispose the extra ice gatherer. @@ -634,61 +498,52 @@ module.exports = function(window, edgeVersion) { } // RTCIceCandidate doesn't have a component, needs to be added cand.component = 1; - // also the usernameFragment. TODO: update SDP to take both variants. - cand.ufrag = iceGatherer.getLocalParameters().usernameFragment; - - var serializedCandidate = SDPUtils.writeCandidate(cand); - event.candidate = Object.assign(event.candidate, - SDPUtils.parseCandidate(serializedCandidate)); - - event.candidate.candidate = serializedCandidate; - event.candidate.toJSON = function() { - return { - candidate: event.candidate.candidate, - sdpMid: event.candidate.sdpMid, - sdpMLineIndex: event.candidate.sdpMLineIndex, - usernameFragment: event.candidate.usernameFragment - }; - }; + event.candidate.candidate = SDPUtils.writeCandidate(cand); } // update local description. - var sections = SDPUtils.getMediaSections(pc._localDescription.sdp); + var sections = SDPUtils.splitSections(self.localDescription.sdp); if (!end) { - sections[event.candidate.sdpMLineIndex] += + sections[event.candidate.sdpMLineIndex + 1] += 'a=' + event.candidate.candidate + '\r\n'; } else { - sections[event.candidate.sdpMLineIndex] += + sections[event.candidate.sdpMLineIndex + 1] += 'a=end-of-candidates\r\n'; } - pc._localDescription.sdp = - SDPUtils.getDescription(pc._localDescription.sdp) + - sections.join(''); - var complete = pc.transceivers.every(function(transceiver) { + self.localDescription.sdp = sections.join(''); + var complete = self.transceivers.every(function(transceiver) { return transceiver.iceGatherer && transceiver.iceGatherer.state === 'completed'; }); - if (pc.iceGatheringState !== 'gathering') { - pc.iceGatheringState = 'gathering'; - pc._emitGatheringStateChange(); + if (self.iceGatheringState !== 'gathering') { + self.iceGatheringState = 'gathering'; + self._emitGatheringStateChange(); } // Emit candidate. Also emit null candidate when all gatherers are // complete. if (!end) { - pc._dispatchEvent('icecandidate', event); + self.dispatchEvent(event); + if (typeof self.onicecandidate === 'function') { + self.onicecandidate(event); + } } if (complete) { - pc._dispatchEvent('icecandidate', new Event('icecandidate')); - pc.iceGatheringState = 'complete'; - pc._emitGatheringStateChange(); + self.dispatchEvent(new Event('icecandidate')); + if (typeof self.onicecandidate === 'function') { + self.onicecandidate(new Event('icecandidate')); + } + self.iceGatheringState = 'complete'; + self._emitGatheringStateChange(); } }; // emit already gathered candidates. window.setTimeout(function() { - bufferedCandidateEvents.forEach(function(e) { + candidates.forEach(function(candidate) { + var e = new Event('RTCIceGatherEvent'); + e.candidate = candidate; iceGatherer.onlocalcandidate(e); }); }, 0); @@ -696,22 +551,21 @@ module.exports = function(window, edgeVersion) { // Create ICE transport and DTLS transport. RTCPeerConnection.prototype._createIceAndDtlsTransports = function() { - var pc = this; + var self = this; var iceTransport = new window.RTCIceTransport(null); iceTransport.onicestatechange = function() { - pc._updateIceConnectionState(); - pc._updateConnectionState(); + self._updateConnectionState(); }; var dtlsTransport = new window.RTCDtlsTransport(iceTransport); dtlsTransport.ondtlsstatechange = function() { - pc._updateConnectionState(); + self._updateConnectionState(); }; dtlsTransport.onerror = function() { // onerror does not set state to failed by itself. Object.defineProperty(dtlsTransport, 'state', {value: 'failed', writable: true}); - pc._updateConnectionState(); + self._updateConnectionState(); }; return { @@ -767,17 +621,11 @@ module.exports = function(window, edgeVersion) { delete p.rtx; }); } - if (transceiver.recvEncodingParameters.length) { - params.encodings = transceiver.recvEncodingParameters; - } else { - params.encodings = [{}]; - } + params.encodings = transceiver.recvEncodingParameters; params.rtcp = { + cname: transceiver.rtcpParameters.cname, compound: transceiver.rtcpParameters.compound }; - if (transceiver.rtcpParameters.cname) { - params.rtcp.cname = transceiver.rtcpParameters.cname; - } if (transceiver.sendEncodingParameters.length) { params.rtcp.ssrc = transceiver.sendEncodingParameters[0].ssrc; } @@ -786,19 +634,20 @@ module.exports = function(window, edgeVersion) { }; RTCPeerConnection.prototype.setLocalDescription = function(description) { - var pc = this; - - // Note: pranswer is not supported. - if (['offer', 'answer'].indexOf(description.type) === -1) { - return Promise.reject(makeError('TypeError', - 'Unsupported type "' + description.type + '"')); - } + var self = this; + var args = arguments; if (!isActionAllowedInSignalingState('setLocalDescription', - description.type, pc.signalingState) || pc._isClosed) { - return Promise.reject(makeError('InvalidStateError', - 'Can not set local ' + description.type + - ' in state ' + pc.signalingState)); + description.type, this.signalingState)) { + return new Promise(function(resolve, reject) { + var e = new Error('Can not set local ' + description.type + + ' in state ' + self.signalingState); + e.name = 'InvalidStateError'; + if (args.length > 2 && typeof args[2] === 'function') { + args[2].apply(null, [e]); + } + reject(e); + }); } var sections; @@ -810,19 +659,19 @@ module.exports = function(window, edgeVersion) { sessionpart = sections.shift(); sections.forEach(function(mediaSection, sdpMLineIndex) { var caps = SDPUtils.parseRtpParameters(mediaSection); - pc.transceivers[sdpMLineIndex].localCapabilities = caps; + self.transceivers[sdpMLineIndex].localCapabilities = caps; }); - pc.transceivers.forEach(function(transceiver, sdpMLineIndex) { - pc._gather(transceiver.mid, sdpMLineIndex); + this.transceivers.forEach(function(transceiver, sdpMLineIndex) { + self._gather(transceiver.mid, sdpMLineIndex); }); } else if (description.type === 'answer') { - sections = SDPUtils.splitSections(pc._remoteDescription.sdp); + sections = SDPUtils.splitSections(self.remoteDescription.sdp); sessionpart = sections.shift(); var isIceLite = SDPUtils.matchPrefix(sessionpart, 'a=ice-lite').length > 0; sections.forEach(function(mediaSection, sdpMLineIndex) { - var transceiver = pc.transceivers[sdpMLineIndex]; + var transceiver = self.transceivers[sdpMLineIndex]; var iceGatherer = transceiver.iceGatherer; var iceTransport = transceiver.iceTransport; var dtlsTransport = transceiver.dtlsTransport; @@ -831,9 +680,9 @@ module.exports = function(window, edgeVersion) { // treat bundle-only as not-rejected. var rejected = SDPUtils.isRejected(mediaSection) && - SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 0; + !SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 1; - if (!rejected && !transceiver.rejected) { + if (!rejected && !transceiver.isDatachannel) { var remoteIceParameters = SDPUtils.getIceParameters( mediaSection, sessionpart); var remoteDtlsParameters = SDPUtils.getDtlsParameters( @@ -842,8 +691,8 @@ module.exports = function(window, edgeVersion) { remoteDtlsParameters.role = 'server'; } - if (!pc.usingBundle || sdpMLineIndex === 0) { - pc._gather(transceiver.mid, sdpMLineIndex); + if (!self.usingBundle || sdpMLineIndex === 0) { + self._gather(transceiver.mid, sdpMLineIndex); if (iceTransport.state === 'new') { iceTransport.start(iceGatherer, remoteIceParameters, isIceLite ? 'controlling' : 'controlled'); @@ -859,44 +708,61 @@ module.exports = function(window, edgeVersion) { // Start the RTCRtpSender. The RTCRtpReceiver for this // transceiver has already been started in setRemoteDescription. - pc._transceive(transceiver, + self._transceive(transceiver, params.codecs.length > 0, false); } }); } - pc._localDescription = { + this.localDescription = { type: description.type, sdp: description.sdp }; - if (description.type === 'offer') { - pc._updateSignalingState('have-local-offer'); - } else { - pc._updateSignalingState('stable'); + switch (description.type) { + case 'offer': + this._updateSignalingState('have-local-offer'); + break; + case 'answer': + this._updateSignalingState('stable'); + break; + default: + throw new TypeError('unsupported type "' + description.type + + '"'); } - return Promise.resolve(); + // If a success callback was provided, emit ICE candidates after it + // has been executed. Otherwise, emit callback after the Promise is + // resolved. + var cb = arguments.length > 1 && typeof arguments[1] === 'function' && + arguments[1]; + return new Promise(function(resolve) { + if (cb) { + cb.apply(null); + } + resolve(); + }); }; RTCPeerConnection.prototype.setRemoteDescription = function(description) { - var pc = this; - - // Note: pranswer is not supported. - if (['offer', 'answer'].indexOf(description.type) === -1) { - return Promise.reject(makeError('TypeError', - 'Unsupported type "' + description.type + '"')); - } + var self = this; + var args = arguments; if (!isActionAllowedInSignalingState('setRemoteDescription', - description.type, pc.signalingState) || pc._isClosed) { - return Promise.reject(makeError('InvalidStateError', - 'Can not set remote ' + description.type + - ' in state ' + pc.signalingState)); + description.type, this.signalingState)) { + return new Promise(function(resolve, reject) { + var e = new Error('Can not set remote ' + description.type + + ' in state ' + self.signalingState); + e.name = 'InvalidStateError'; + if (args.length > 2 && typeof args[2] === 'function') { + args[2].apply(null, [e]); + } + reject(e); + }); } var streams = {}; - pc.remoteStreams.forEach(function(stream) { + this.remoteStreams.forEach(function(stream) { streams[stream.id] = stream; }); var receiverList = []; @@ -906,14 +772,14 @@ module.exports = function(window, edgeVersion) { 'a=ice-lite').length > 0; var usingBundle = SDPUtils.matchPrefix(sessionpart, 'a=group:BUNDLE ').length > 0; - pc.usingBundle = usingBundle; + this.usingBundle = usingBundle; var iceOptions = SDPUtils.matchPrefix(sessionpart, 'a=ice-options:')[0]; if (iceOptions) { - pc.canTrickleIceCandidates = iceOptions.substr(14).split(' ') + this.canTrickleIceCandidates = iceOptions.substr(14).split(' ') .indexOf('trickle') >= 0; } else { - pc.canTrickleIceCandidates = false; + this.canTrickleIceCandidates = false; } sections.forEach(function(mediaSection, sdpMLineIndex) { @@ -921,7 +787,7 @@ module.exports = function(window, edgeVersion) { var kind = SDPUtils.getKind(mediaSection); // treat bundle-only as not-rejected. var rejected = SDPUtils.isRejected(mediaSection) && - SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 0; + !SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 1; var protocol = lines[0].substr(2).split(' ')[2]; var direction = SDPUtils.getDirection(mediaSection, sessionpart); @@ -930,25 +796,14 @@ module.exports = function(window, edgeVersion) { var mid = SDPUtils.getMid(mediaSection) || SDPUtils.generateIdentifier(); // Reject datachannels which are not implemented yet. - if (rejected || (kind === 'application' && (protocol === 'DTLS/SCTP' || - protocol === 'UDP/DTLS/SCTP'))) { - // TODO: this is dangerous in the case where a non-rejected m-line - // becomes rejected. - pc.transceivers[sdpMLineIndex] = { + if (kind === 'application' && protocol === 'DTLS/SCTP') { + self.transceivers[sdpMLineIndex] = { mid: mid, - kind: kind, - protocol: protocol, - rejected: true + isDatachannel: true }; return; } - if (!rejected && pc.transceivers[sdpMLineIndex] && - pc.transceivers[sdpMLineIndex].rejected) { - // recycle a rejected transceiver. - pc.transceivers[sdpMLineIndex] = pc._createTransceiver(kind, true); - } - var transceiver; var iceGatherer; var iceTransport; @@ -988,30 +843,30 @@ module.exports = function(window, edgeVersion) { // Check if we can use BUNDLE and dispose transports. if ((description.type === 'offer' || description.type === 'answer') && !rejected && usingBundle && sdpMLineIndex > 0 && - pc.transceivers[sdpMLineIndex]) { - pc._disposeIceAndDtlsTransports(sdpMLineIndex); - pc.transceivers[sdpMLineIndex].iceGatherer = - pc.transceivers[0].iceGatherer; - pc.transceivers[sdpMLineIndex].iceTransport = - pc.transceivers[0].iceTransport; - pc.transceivers[sdpMLineIndex].dtlsTransport = - pc.transceivers[0].dtlsTransport; - if (pc.transceivers[sdpMLineIndex].rtpSender) { - pc.transceivers[sdpMLineIndex].rtpSender.setTransport( - pc.transceivers[0].dtlsTransport); + self.transceivers[sdpMLineIndex]) { + self._disposeIceAndDtlsTransports(sdpMLineIndex); + self.transceivers[sdpMLineIndex].iceGatherer = + self.transceivers[0].iceGatherer; + self.transceivers[sdpMLineIndex].iceTransport = + self.transceivers[0].iceTransport; + self.transceivers[sdpMLineIndex].dtlsTransport = + self.transceivers[0].dtlsTransport; + if (self.transceivers[sdpMLineIndex].rtpSender) { + self.transceivers[sdpMLineIndex].rtpSender.setTransport( + self.transceivers[0].dtlsTransport); } - if (pc.transceivers[sdpMLineIndex].rtpReceiver) { - pc.transceivers[sdpMLineIndex].rtpReceiver.setTransport( - pc.transceivers[0].dtlsTransport); + if (self.transceivers[sdpMLineIndex].rtpReceiver) { + self.transceivers[sdpMLineIndex].rtpReceiver.setTransport( + self.transceivers[0].dtlsTransport); } } if (description.type === 'offer' && !rejected) { - transceiver = pc.transceivers[sdpMLineIndex] || - pc._createTransceiver(kind); + transceiver = self.transceivers[sdpMLineIndex] || + self._createTransceiver(kind); transceiver.mid = mid; if (!transceiver.iceGatherer) { - transceiver.iceGatherer = pc._createIceGatherer(sdpMLineIndex, + transceiver.iceGatherer = self._createIceGatherer(sdpMLineIndex, usingBundle); } @@ -1040,7 +895,6 @@ module.exports = function(window, edgeVersion) { ssrc: (2 * sdpMLineIndex + 2) * 1001 }]; - // TODO: rewrite to use http://w3c.github.io/webrtc-pc/#set-associated-remote-streams var isNewTrack = false; if (direction === 'sendrecv' || direction === 'sendonly') { isNewTrack = !transceiver.rtpReceiver; @@ -1051,9 +905,7 @@ module.exports = function(window, edgeVersion) { var stream; track = rtpReceiver.track; // FIXME: does not work with Plan B. - if (remoteMsid && remoteMsid.stream === '-') { - // no-op. a stream id of '-' means: no associated stream. - } else if (remoteMsid) { + if (remoteMsid) { if (!streams[remoteMsid.stream]) { streams[remoteMsid.stream] = new window.MediaStream(); Object.defineProperty(streams[remoteMsid.stream], 'id', { @@ -1074,22 +926,9 @@ module.exports = function(window, edgeVersion) { } stream = streams.default; } - if (stream) { - addTrackToStreamAndFireEvent(track, stream); - transceiver.associatedRemoteMediaStreams.push(stream); - } + stream.addTrack(track); receiverList.push([track, rtpReceiver, stream]); } - } else if (transceiver.rtpReceiver && transceiver.rtpReceiver.track) { - transceiver.associatedRemoteMediaStreams.forEach(function(s) { - var nativeTrack = s.getTracks().find(function(t) { - return t.id === transceiver.rtpReceiver.track.id; - }); - if (nativeTrack) { - removeTrackFromStreamAndFireEvent(nativeTrack, s); - } - }); - transceiver.associatedRemoteMediaStreams = []; } transceiver.localCapabilities = localCapabilities; @@ -1101,11 +940,11 @@ module.exports = function(window, edgeVersion) { // Start the RTCRtpReceiver now. The RTPSender is started in // setLocalDescription. - pc._transceive(pc.transceivers[sdpMLineIndex], + self._transceive(self.transceivers[sdpMLineIndex], false, isNewTrack); } else if (description.type === 'answer' && !rejected) { - transceiver = pc.transceivers[sdpMLineIndex]; + transceiver = self.transceivers[sdpMLineIndex]; iceGatherer = transceiver.iceGatherer; iceTransport = transceiver.iceTransport; dtlsTransport = transceiver.dtlsTransport; @@ -1113,11 +952,11 @@ module.exports = function(window, edgeVersion) { sendEncodingParameters = transceiver.sendEncodingParameters; localCapabilities = transceiver.localCapabilities; - pc.transceivers[sdpMLineIndex].recvEncodingParameters = + self.transceivers[sdpMLineIndex].recvEncodingParameters = recvEncodingParameters; - pc.transceivers[sdpMLineIndex].remoteCapabilities = + self.transceivers[sdpMLineIndex].remoteCapabilities = remoteCapabilities; - pc.transceivers[sdpMLineIndex].rtcpParameters = rtcpParameters; + self.transceivers[sdpMLineIndex].rtcpParameters = rtcpParameters; if (cands.length && iceTransport.state === 'new') { if ((isIceLite || isComplete) && @@ -1140,24 +979,10 @@ module.exports = function(window, edgeVersion) { } } - // If the offer contained RTX but the answer did not, - // remove RTX from sendEncodingParameters. - var commonCapabilities = getCommonCapabilities( - transceiver.localCapabilities, - transceiver.remoteCapabilities); - - var hasRtx = commonCapabilities.codecs.filter(function(c) { - return c.name.toLowerCase() === 'rtx'; - }).length; - if (!hasRtx && transceiver.sendEncodingParameters[0].rtx) { - delete transceiver.sendEncodingParameters[0].rtx; - } - - pc._transceive(transceiver, + self._transceive(transceiver, direction === 'sendrecv' || direction === 'recvonly', direction === 'sendrecv' || direction === 'sendonly'); - // TODO: rewrite to use http://w3c.github.io/webrtc-pc/#set-associated-remote-streams if (rtpReceiver && (direction === 'sendrecv' || direction === 'sendonly')) { track = rtpReceiver.track; @@ -1165,13 +990,13 @@ module.exports = function(window, edgeVersion) { if (!streams[remoteMsid.stream]) { streams[remoteMsid.stream] = new window.MediaStream(); } - addTrackToStreamAndFireEvent(track, streams[remoteMsid.stream]); + streams[remoteMsid.stream].addTrack(track); receiverList.push([track, rtpReceiver, streams[remoteMsid.stream]]); } else { if (!streams.default) { streams.default = new window.MediaStream(); } - addTrackToStreamAndFireEvent(track, streams.default); + streams.default.addTrack(track); receiverList.push([track, rtpReceiver, streams.default]); } } else { @@ -1181,28 +1006,37 @@ module.exports = function(window, edgeVersion) { } }); - if (pc._dtlsRole === undefined) { - pc._dtlsRole = description.type === 'offer' ? 'active' : 'passive'; + if (this._dtlsRole === undefined) { + this._dtlsRole = description.type === 'offer' ? 'active' : 'passive'; } - pc._remoteDescription = { + this.remoteDescription = { type: description.type, sdp: description.sdp }; - if (description.type === 'offer') { - pc._updateSignalingState('have-remote-offer'); - } else { - pc._updateSignalingState('stable'); + switch (description.type) { + case 'offer': + this._updateSignalingState('have-remote-offer'); + break; + case 'answer': + this._updateSignalingState('stable'); + break; + default: + throw new TypeError('unsupported type "' + description.type + + '"'); } Object.keys(streams).forEach(function(sid) { var stream = streams[sid]; if (stream.getTracks().length) { - if (pc.remoteStreams.indexOf(stream) === -1) { - pc.remoteStreams.push(stream); + if (self.remoteStreams.indexOf(stream) === -1) { + self.remoteStreams.push(stream); var event = new Event('addstream'); event.stream = stream; window.setTimeout(function() { - pc._dispatchEvent('addstream', event); + self.dispatchEvent(event); + if (typeof self.onaddstream === 'function') { + self.onaddstream(event); + } }); } @@ -1212,24 +1046,28 @@ module.exports = function(window, edgeVersion) { if (stream.id !== item[2].id) { return; } - fireAddTrack(pc, track, receiver, [stream]); + var trackEvent = new Event('track'); + trackEvent.track = track; + trackEvent.receiver = receiver; + trackEvent.transceiver = {receiver: receiver}; + trackEvent.streams = [stream]; + window.setTimeout(function() { + self.dispatchEvent(trackEvent); + if (typeof self.ontrack === 'function') { + self.ontrack(trackEvent); + } + }); }); } }); - receiverList.forEach(function(item) { - if (item[2]) { - return; - } - fireAddTrack(pc, item[0], item[1], []); - }); // check whether addIceCandidate({}) was called within four seconds after // setRemoteDescription. window.setTimeout(function() { - if (!(pc && pc.transceivers)) { + if (!(self && self.transceivers)) { return; } - pc.transceivers.forEach(function(transceiver) { + self.transceivers.forEach(function(transceiver) { if (transceiver.iceTransport && transceiver.iceTransport.state === 'new' && transceiver.iceTransport.getRemoteCandidates().length > 0) { @@ -1240,7 +1078,12 @@ module.exports = function(window, edgeVersion) { }); }, 4000); - return Promise.resolve(); + return new Promise(function(resolve) { + if (args.length > 1 && typeof args[1] === 'function') { + args[1].apply(null); + } + resolve(); + }); }; RTCPeerConnection.prototype.close = function() { @@ -1264,7 +1107,6 @@ module.exports = function(window, edgeVersion) { } }); // FIXME: clean up tracks, local streams, remote streams, etc - this._isClosed = true; this._updateSignalingState('closed'); }; @@ -1272,63 +1114,32 @@ module.exports = function(window, edgeVersion) { RTCPeerConnection.prototype._updateSignalingState = function(newState) { this.signalingState = newState; var event = new Event('signalingstatechange'); - this._dispatchEvent('signalingstatechange', event); + this.dispatchEvent(event); + if (typeof this.onsignalingstatechange === 'function') { + this.onsignalingstatechange(event); + } }; // Determine whether to fire the negotiationneeded event. RTCPeerConnection.prototype._maybeFireNegotiationNeeded = function() { - var pc = this; + var self = this; if (this.signalingState !== 'stable' || this.needNegotiation === true) { return; } this.needNegotiation = true; window.setTimeout(function() { - if (pc.needNegotiation) { - pc.needNegotiation = false; - var event = new Event('negotiationneeded'); - pc._dispatchEvent('negotiationneeded', event); + if (self.needNegotiation === false) { + return; + } + self.needNegotiation = false; + var event = new Event('negotiationneeded'); + self.dispatchEvent(event); + if (typeof self.onnegotiationneeded === 'function') { + self.onnegotiationneeded(event); } }, 0); }; - // Update the ice connection state. - RTCPeerConnection.prototype._updateIceConnectionState = function() { - var newState; - var states = { - 'new': 0, - closed: 0, - checking: 0, - connected: 0, - completed: 0, - disconnected: 0, - failed: 0 - }; - this.transceivers.forEach(function(transceiver) { - states[transceiver.iceTransport.state]++; - }); - - newState = 'new'; - if (states.failed > 0) { - newState = 'failed'; - } else if (states.checking > 0) { - newState = 'checking'; - } else if (states.disconnected > 0) { - newState = 'disconnected'; - } else if (states.new > 0) { - newState = 'new'; - } else if (states.connected > 0) { - newState = 'connected'; - } else if (states.completed > 0) { - newState = 'completed'; - } - - if (newState !== this.iceConnectionState) { - this.iceConnectionState = newState; - var event = new Event('iceconnectionstatechange'); - this._dispatchEvent('iceconnectionstatechange', event); - } - }; - // Update the connection state. RTCPeerConnection.prototype._updateConnectionState = function() { var newState; @@ -1336,6 +1147,7 @@ module.exports = function(window, edgeVersion) { 'new': 0, closed: 0, connecting: 0, + checking: 0, connected: 0, completed: 0, disconnected: 0, @@ -1351,40 +1163,45 @@ module.exports = function(window, edgeVersion) { newState = 'new'; if (states.failed > 0) { newState = 'failed'; - } else if (states.connecting > 0) { + } else if (states.connecting > 0 || states.checking > 0) { newState = 'connecting'; } else if (states.disconnected > 0) { newState = 'disconnected'; } else if (states.new > 0) { newState = 'new'; - } else if (states.connected > 0) { + } else if (states.connected > 0 || states.completed > 0) { newState = 'connected'; } - if (newState !== this.connectionState) { - this.connectionState = newState; - var event = new Event('connectionstatechange'); - this._dispatchEvent('connectionstatechange', event); + if (newState !== this.iceConnectionState) { + this.iceConnectionState = newState; + var event = new Event('iceconnectionstatechange'); + this.dispatchEvent(event); + if (typeof this.oniceconnectionstatechange === 'function') { + this.oniceconnectionstatechange(event); + } } }; RTCPeerConnection.prototype.createOffer = function() { - var pc = this; + var self = this; + var args = arguments; - if (pc._isClosed) { - return Promise.reject(makeError('InvalidStateError', - 'Can not call createOffer after close')); + var offerOptions; + if (arguments.length === 1 && typeof arguments[0] !== 'function') { + offerOptions = arguments[0]; + } else if (arguments.length === 3) { + offerOptions = arguments[2]; } - var numAudioTracks = pc.transceivers.filter(function(t) { + var numAudioTracks = this.transceivers.filter(function(t) { return t.kind === 'audio'; }).length; - var numVideoTracks = pc.transceivers.filter(function(t) { + var numVideoTracks = this.transceivers.filter(function(t) { return t.kind === 'video'; }).length; // Determine number of audio and video tracks we need to send/recv. - var offerOptions = arguments[0]; if (offerOptions) { // Reject Chrome legacy constraints. if (offerOptions.mandatory || offerOptions.optional) { @@ -1411,7 +1228,7 @@ module.exports = function(window, edgeVersion) { } } - pc.transceivers.forEach(function(transceiver) { + this.transceivers.forEach(function(transceiver) { if (transceiver.kind === 'audio') { numAudioTracks--; if (numAudioTracks < 0) { @@ -1428,28 +1245,28 @@ module.exports = function(window, edgeVersion) { // Create M-lines for recvonly streams. while (numAudioTracks > 0 || numVideoTracks > 0) { if (numAudioTracks > 0) { - pc._createTransceiver('audio'); + this._createTransceiver('audio'); numAudioTracks--; } if (numVideoTracks > 0) { - pc._createTransceiver('video'); + this._createTransceiver('video'); numVideoTracks--; } } - var sdp = SDPUtils.writeSessionBoilerplate(pc._sdpSessionId, - pc._sdpSessionVersion++); - pc.transceivers.forEach(function(transceiver, sdpMLineIndex) { + var sdp = SDPUtils.writeSessionBoilerplate(this._sdpSessionId, + this._sdpSessionVersion++); + this.transceivers.forEach(function(transceiver, sdpMLineIndex) { // For each track, create an ice gatherer, ice transport, // dtls transport, potentially rtpsender and rtpreceiver. var track = transceiver.track; var kind = transceiver.kind; - var mid = transceiver.mid || SDPUtils.generateIdentifier(); + var mid = SDPUtils.generateIdentifier(); transceiver.mid = mid; if (!transceiver.iceGatherer) { - transceiver.iceGatherer = pc._createIceGatherer(sdpMLineIndex, - pc.usingBundle); + transceiver.iceGatherer = self._createIceGatherer(sdpMLineIndex, + self.usingBundle); } var localCapabilities = window.RTCRtpSender.getCapabilities(kind); @@ -1468,27 +1285,6 @@ module.exports = function(window, edgeVersion) { codec.parameters['level-asymmetry-allowed'] === undefined) { codec.parameters['level-asymmetry-allowed'] = '1'; } - - // for subsequent offers, we might have to re-use the payload - // type of the last offer. - if (transceiver.remoteCapabilities && - transceiver.remoteCapabilities.codecs) { - transceiver.remoteCapabilities.codecs.forEach(function(remoteCodec) { - if (codec.name.toLowerCase() === remoteCodec.name.toLowerCase() && - codec.clockRate === remoteCodec.clockRate) { - codec.preferredPayloadType = remoteCodec.payloadType; - } - }); - } - }); - localCapabilities.headerExtensions.forEach(function(hdrExt) { - var remoteExtensions = transceiver.remoteCapabilities && - transceiver.remoteCapabilities.headerExtensions || []; - remoteExtensions.forEach(function(rHdrExt) { - if (hdrExt.uri === rHdrExt.uri) { - hdrExt.id = rHdrExt.id; - } - }); }); // generate an ssrc now, to be used later in rtpSender.send @@ -1515,20 +1311,20 @@ module.exports = function(window, edgeVersion) { }); // always offer BUNDLE and dispose on return if not supported. - if (pc._config.bundlePolicy !== 'max-compat') { - sdp += 'a=group:BUNDLE ' + pc.transceivers.map(function(t) { + if (this._config.bundlePolicy !== 'max-compat') { + sdp += 'a=group:BUNDLE ' + this.transceivers.map(function(t) { return t.mid; }).join(' ') + '\r\n'; } sdp += 'a=ice-options:trickle\r\n'; - pc.transceivers.forEach(function(transceiver, sdpMLineIndex) { + this.transceivers.forEach(function(transceiver, sdpMLineIndex) { sdp += writeMediaSection(transceiver, transceiver.localCapabilities, - 'offer', transceiver.stream, pc._dtlsRole); + 'offer', transceiver.stream, self._dtlsRole); sdp += 'a=rtcp-rsize\r\n'; - if (transceiver.iceGatherer && pc.iceGatheringState !== 'new' && - (sdpMLineIndex === 0 || !pc.usingBundle)) { + if (transceiver.iceGatherer && self.iceGatheringState !== 'new' && + (sdpMLineIndex === 0 || !self.usingBundle)) { transceiver.iceGatherer.getLocalCandidates().forEach(function(cand) { cand.component = 1; sdp += 'a=' + SDPUtils.writeCandidate(cand) + '\r\n'; @@ -1544,55 +1340,36 @@ module.exports = function(window, edgeVersion) { type: 'offer', sdp: sdp }); - return Promise.resolve(desc); + return new Promise(function(resolve) { + if (args.length > 0 && typeof args[0] === 'function') { + args[0].apply(null, [desc]); + resolve(); + return; + } + resolve(desc); + }); }; RTCPeerConnection.prototype.createAnswer = function() { - var pc = this; - - if (pc._isClosed) { - return Promise.reject(makeError('InvalidStateError', - 'Can not call createAnswer after close')); - } - - if (!(pc.signalingState === 'have-remote-offer' || - pc.signalingState === 'have-local-pranswer')) { - return Promise.reject(makeError('InvalidStateError', - 'Can not call createAnswer in signalingState ' + pc.signalingState)); - } + var self = this; + var args = arguments; - var sdp = SDPUtils.writeSessionBoilerplate(pc._sdpSessionId, - pc._sdpSessionVersion++); - if (pc.usingBundle) { - sdp += 'a=group:BUNDLE ' + pc.transceivers.map(function(t) { + var sdp = SDPUtils.writeSessionBoilerplate(this._sdpSessionId, + this._sdpSessionVersion++); + if (this.usingBundle) { + sdp += 'a=group:BUNDLE ' + this.transceivers.map(function(t) { return t.mid; }).join(' ') + '\r\n'; } - sdp += 'a=ice-options:trickle\r\n'; - - var mediaSectionsInOffer = SDPUtils.getMediaSections( - pc._remoteDescription.sdp).length; - pc.transceivers.forEach(function(transceiver, sdpMLineIndex) { + var mediaSectionsInOffer = SDPUtils.splitSections( + this.remoteDescription.sdp).length - 1; + this.transceivers.forEach(function(transceiver, sdpMLineIndex) { if (sdpMLineIndex + 1 > mediaSectionsInOffer) { return; } - if (transceiver.rejected) { - if (transceiver.kind === 'application') { - if (transceiver.protocol === 'DTLS/SCTP') { // legacy fmt - sdp += 'm=application 0 DTLS/SCTP 5000\r\n'; - } else { - sdp += 'm=application 0 ' + transceiver.protocol + - ' webrtc-datachannel\r\n'; - } - } else if (transceiver.kind === 'audio') { - sdp += 'm=audio 0 UDP/TLS/RTP/SAVPF 0\r\n' + - 'a=rtpmap:0 PCMU/8000\r\n'; - } else if (transceiver.kind === 'video') { - sdp += 'm=video 0 UDP/TLS/RTP/SAVPF 120\r\n' + - 'a=rtpmap:120 VP8/90000\r\n'; - } - sdp += 'c=IN IP4 0.0.0.0\r\n' + - 'a=inactive\r\n' + + if (transceiver.isDatachannel) { + sdp += 'm=application 0 DTLS/SCTP 5000\r\n' + + 'c=IN IP4 0.0.0.0\r\n' + 'a=mid:' + transceiver.mid + '\r\n'; return; } @@ -1629,7 +1406,7 @@ module.exports = function(window, edgeVersion) { } sdp += writeMediaSection(transceiver, commonCapabilities, - 'answer', transceiver.stream, pc._dtlsRole); + 'answer', transceiver.stream, self._dtlsRole); if (transceiver.rtcpParameters && transceiver.rtcpParameters.reducedSize) { sdp += 'a=rtcp-rsize\r\n'; @@ -1640,111 +1417,107 @@ module.exports = function(window, edgeVersion) { type: 'answer', sdp: sdp }); - return Promise.resolve(desc); + return new Promise(function(resolve) { + if (args.length > 0 && typeof args[0] === 'function') { + args[0].apply(null, [desc]); + resolve(); + return; + } + resolve(desc); + }); }; RTCPeerConnection.prototype.addIceCandidate = function(candidate) { - var pc = this; + var err; var sections; - if (candidate && !(candidate.sdpMLineIndex !== undefined || - candidate.sdpMid)) { - return Promise.reject(new TypeError('sdpMLineIndex or sdpMid required')); - } - - // TODO: needs to go into ops queue. - return new Promise(function(resolve, reject) { - if (!pc._remoteDescription) { - return reject(makeError('InvalidStateError', - 'Can not add ICE candidate without a remote description')); - } else if (!candidate || candidate.candidate === '') { - for (var j = 0; j < pc.transceivers.length; j++) { - if (pc.transceivers[j].rejected) { - continue; - } - pc.transceivers[j].iceTransport.addRemoteCandidate({}); - sections = SDPUtils.getMediaSections(pc._remoteDescription.sdp); - sections[j] += 'a=end-of-candidates\r\n'; - pc._remoteDescription.sdp = - SDPUtils.getDescription(pc._remoteDescription.sdp) + - sections.join(''); - if (pc.usingBundle) { + if (!candidate || candidate.candidate === '') { + for (var j = 0; j < this.transceivers.length; j++) { + if (this.transceivers[j].isDatachannel) { + continue; + } + this.transceivers[j].iceTransport.addRemoteCandidate({}); + sections = SDPUtils.splitSections(this.remoteDescription.sdp); + sections[j + 1] += 'a=end-of-candidates\r\n'; + this.remoteDescription.sdp = sections.join(''); + if (this.usingBundle) { + break; + } + } + } else if (!(candidate.sdpMLineIndex !== undefined || candidate.sdpMid)) { + throw new TypeError('sdpMLineIndex or sdpMid required'); + } else if (!this.remoteDescription) { + err = new Error('Can not add ICE candidate without ' + + 'a remote description'); + err.name = 'InvalidStateError'; + } else { + var sdpMLineIndex = candidate.sdpMLineIndex; + if (candidate.sdpMid) { + for (var i = 0; i < this.transceivers.length; i++) { + if (this.transceivers[i].mid === candidate.sdpMid) { + sdpMLineIndex = i; break; } } - } else { - var sdpMLineIndex = candidate.sdpMLineIndex; - if (candidate.sdpMid) { - for (var i = 0; i < pc.transceivers.length; i++) { - if (pc.transceivers[i].mid === candidate.sdpMid) { - sdpMLineIndex = i; - break; - } - } + } + var transceiver = this.transceivers[sdpMLineIndex]; + if (transceiver) { + if (transceiver.isDatachannel) { + return Promise.resolve(); } - var transceiver = pc.transceivers[sdpMLineIndex]; - if (transceiver) { - if (transceiver.rejected) { - return resolve(); - } - var cand = Object.keys(candidate.candidate).length > 0 ? - SDPUtils.parseCandidate(candidate.candidate) : {}; - // Ignore Chrome's invalid candidates since Edge does not like them. - if (cand.protocol === 'tcp' && (cand.port === 0 || cand.port === 9)) { - return resolve(); - } - // Ignore RTCP candidates, we assume RTCP-MUX. - if (cand.component && cand.component !== 1) { - return resolve(); - } - // when using bundle, avoid adding candidates to the wrong - // ice transport. And avoid adding candidates added in the SDP. - if (sdpMLineIndex === 0 || (sdpMLineIndex > 0 && - transceiver.iceTransport !== pc.transceivers[0].iceTransport)) { - if (!maybeAddCandidate(transceiver.iceTransport, cand)) { - return reject(makeError('OperationError', - 'Can not add ICE candidate')); - } + var cand = Object.keys(candidate.candidate).length > 0 ? + SDPUtils.parseCandidate(candidate.candidate) : {}; + // Ignore Chrome's invalid candidates since Edge does not like them. + if (cand.protocol === 'tcp' && (cand.port === 0 || cand.port === 9)) { + return Promise.resolve(); + } + // Ignore RTCP candidates, we assume RTCP-MUX. + if (cand.component && cand.component !== 1) { + return Promise.resolve(); + } + // when using bundle, avoid adding candidates to the wrong + // ice transport. And avoid adding candidates added in the SDP. + if (sdpMLineIndex === 0 || (sdpMLineIndex > 0 && + transceiver.iceTransport !== this.transceivers[0].iceTransport)) { + if (!maybeAddCandidate(transceiver.iceTransport, cand)) { + err = new Error('Can not add ICE candidate'); + err.name = 'OperationError'; } + } + if (!err) { // update the remoteDescription. var candidateString = candidate.candidate.trim(); if (candidateString.indexOf('a=') === 0) { candidateString = candidateString.substr(2); } - sections = SDPUtils.getMediaSections(pc._remoteDescription.sdp); - sections[sdpMLineIndex] += 'a=' + + sections = SDPUtils.splitSections(this.remoteDescription.sdp); + sections[sdpMLineIndex + 1] += 'a=' + (cand.type ? candidateString : 'end-of-candidates') + '\r\n'; - pc._remoteDescription.sdp = - SDPUtils.getDescription(pc._remoteDescription.sdp) + - sections.join(''); - } else { - return reject(makeError('OperationError', - 'Can not add ICE candidate')); + this.remoteDescription.sdp = sections.join(''); + } + } else { + err = new Error('Can not add ICE candidate'); + err.name = 'OperationError'; + } + } + var args = arguments; + return new Promise(function(resolve, reject) { + if (err) { + if (args.length > 2 && typeof args[2] === 'function') { + args[2].apply(null, [err]); } + reject(err); + } else { + if (args.length > 1 && typeof args[1] === 'function') { + args[1].apply(null); + } + resolve(); } - resolve(); }); }; - RTCPeerConnection.prototype.getStats = function(selector) { - if (selector && selector instanceof window.MediaStreamTrack) { - var senderOrReceiver = null; - this.transceivers.forEach(function(transceiver) { - if (transceiver.rtpSender && - transceiver.rtpSender.track === selector) { - senderOrReceiver = transceiver.rtpSender; - } else if (transceiver.rtpReceiver && - transceiver.rtpReceiver.track === selector) { - senderOrReceiver = transceiver.rtpReceiver; - } - }); - if (!senderOrReceiver) { - throw makeError('InvalidAccessError', 'Invalid selector.'); - } - return senderOrReceiver.getStats(); - } - + RTCPeerConnection.prototype.getStats = function() { var promises = []; this.transceivers.forEach(function(transceiver) { ['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport', @@ -1754,101 +1527,34 @@ module.exports = function(window, edgeVersion) { } }); }); - return Promise.all(promises).then(function(allStats) { + var cb = arguments.length > 1 && typeof arguments[1] === 'function' && + arguments[1]; + var fixStatsType = function(stat) { + return { + inboundrtp: 'inbound-rtp', + outboundrtp: 'outbound-rtp', + candidatepair: 'candidate-pair', + localcandidate: 'local-candidate', + remotecandidate: 'remote-candidate' + }[stat.type] || stat.type; + }; + return new Promise(function(resolve) { + // shim getStats with maplike support var results = new Map(); - allStats.forEach(function(stats) { - stats.forEach(function(stat) { - results.set(stat.id, stat); + Promise.all(promises).then(function(res) { + res.forEach(function(result) { + Object.keys(result).forEach(function(id) { + result[id].type = fixStatsType(result[id]); + results.set(id, result[id]); + }); }); + if (cb) { + cb.apply(null, results); + } + resolve(results); }); - return results; }); }; - - // fix low-level stat names and return Map instead of object. - var ortcObjects = ['RTCRtpSender', 'RTCRtpReceiver', 'RTCIceGatherer', - 'RTCIceTransport', 'RTCDtlsTransport']; - ortcObjects.forEach(function(ortcObjectName) { - var obj = window[ortcObjectName]; - if (obj && obj.prototype && obj.prototype.getStats) { - var nativeGetstats = obj.prototype.getStats; - obj.prototype.getStats = function() { - return nativeGetstats.apply(this) - .then(function(nativeStats) { - var mapStats = new Map(); - Object.keys(nativeStats).forEach(function(id) { - nativeStats[id].type = fixStatsType(nativeStats[id]); - mapStats.set(id, nativeStats[id]); - }); - return mapStats; - }); - }; - } - }); - - // legacy callback shims. Should be moved to adapter.js some days. - var methods = ['createOffer', 'createAnswer']; - methods.forEach(function(method) { - var nativeMethod = RTCPeerConnection.prototype[method]; - RTCPeerConnection.prototype[method] = function() { - var args = arguments; - if (typeof args[0] === 'function' || - typeof args[1] === 'function') { // legacy - return nativeMethod.apply(this, [arguments[2]]) - .then(function(description) { - if (typeof args[0] === 'function') { - args[0].apply(null, [description]); - } - }, function(error) { - if (typeof args[1] === 'function') { - args[1].apply(null, [error]); - } - }); - } - return nativeMethod.apply(this, arguments); - }; - }); - - methods = ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']; - methods.forEach(function(method) { - var nativeMethod = RTCPeerConnection.prototype[method]; - RTCPeerConnection.prototype[method] = function() { - var args = arguments; - if (typeof args[1] === 'function' || - typeof args[2] === 'function') { // legacy - return nativeMethod.apply(this, arguments) - .then(function() { - if (typeof args[1] === 'function') { - args[1].apply(null); - } - }, function(error) { - if (typeof args[2] === 'function') { - args[2].apply(null, [error]); - } - }); - } - return nativeMethod.apply(this, arguments); - }; - }); - - // getStats is special. It doesn't have a spec legacy method yet we support - // getStats(something, cb) without error callbacks. - ['getStats'].forEach(function(method) { - var nativeMethod = RTCPeerConnection.prototype[method]; - RTCPeerConnection.prototype[method] = function() { - var args = arguments; - if (typeof args[1] === 'function') { - return nativeMethod.apply(this, arguments) - .then(function() { - if (typeof args[1] === 'function') { - args[1].apply(null); - } - }); - } - return nativeMethod.apply(this, arguments); - }; - }); - return RTCPeerConnection; }; @@ -1882,19 +1588,6 @@ SDPUtils.splitSections = function(blob) { }); }; -// returns the session description. -SDPUtils.getDescription = function(blob) { - var sections = SDPUtils.splitSections(blob); - return sections && sections[0]; -}; - -// returns the individual media sections. -SDPUtils.getMediaSections = function(blob) { - var sections = SDPUtils.splitSections(blob); - sections.shift(); - return sections; -}; - // Returns lines that start with a certain prefix. SDPUtils.matchPrefix = function(blob, prefix) { return SDPUtils.splitLines(blob).filter(function(line) { @@ -1920,7 +1613,6 @@ SDPUtils.parseCandidate = function(line) { protocol: parts[2].toLowerCase(), priority: parseInt(parts[3], 10), ip: parts[4], - address: parts[4], // address is an alias for ip. port: parseInt(parts[5], 10), // skip parts[6] == 'typ' type: parts[7] @@ -1956,7 +1648,7 @@ SDPUtils.writeCandidate = function(candidate) { sdp.push(candidate.component); sdp.push(candidate.protocol.toUpperCase()); sdp.push(candidate.priority); - sdp.push(candidate.address || candidate.ip); + sdp.push(candidate.ip); sdp.push(candidate.port); var type = candidate.type; @@ -1965,17 +1657,17 @@ SDPUtils.writeCandidate = function(candidate) { if (type !== 'host' && candidate.relatedAddress && candidate.relatedPort) { sdp.push('raddr'); - sdp.push(candidate.relatedAddress); + sdp.push(candidate.relatedAddress); // was: relAddr sdp.push('rport'); - sdp.push(candidate.relatedPort); + sdp.push(candidate.relatedPort); // was: relPort } if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') { sdp.push('tcptype'); sdp.push(candidate.tcpType); } - if (candidate.usernameFragment || candidate.ufrag) { + if (candidate.ufrag) { sdp.push('ufrag'); - sdp.push(candidate.usernameFragment || candidate.ufrag); + sdp.push(candidate.ufrag); } return 'candidate:' + sdp.join(' '); }; @@ -1984,7 +1676,7 @@ SDPUtils.writeCandidate = function(candidate) { // a=ice-options:foo bar SDPUtils.parseIceOptions = function(line) { return line.substr(14).split(' '); -}; +} // Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input: // a=rtpmap:111 opus/48000/2 @@ -1998,9 +1690,8 @@ SDPUtils.parseRtpMap = function(line) { parsed.name = parts[0]; parsed.clockRate = parseInt(parts[1], 10); // was: clockrate - parsed.channels = parts.length === 3 ? parseInt(parts[2], 10) : 1; - // legacy alias, got renamed back to channels in ORTC. - parsed.numChannels = parsed.channels; + // was: channels + parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1; return parsed; }; @@ -2011,9 +1702,8 @@ SDPUtils.writeRtpMap = function(codec) { if (codec.preferredPayloadType !== undefined) { pt = codec.preferredPayloadType; } - var channels = codec.channels || codec.numChannels || 1; return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate + - (channels !== 1 ? '/' + channels : '') + '\r\n'; + (codec.numChannels !== 1 ? '/' + codec.numChannels : '') + '\r\n'; }; // Parses an a=extmap line (headerextension from RFC 5285). Sample input: @@ -2062,11 +1752,7 @@ SDPUtils.writeFmtp = function(codec) { if (codec.parameters && Object.keys(codec.parameters).length) { var params = []; Object.keys(codec.parameters).forEach(function(param) { - if (codec.parameters[param]) { - params.push(param + '=' + codec.parameters[param]); - } else { - params.push(param); - } + params.push(param + '=' + codec.parameters[param]); }); line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n'; } @@ -2117,16 +1803,6 @@ SDPUtils.parseSsrcMedia = function(line) { return parts; }; -SDPUtils.parseSsrcGroup = function(line) { - var parts = line.substr(13).split(' '); - return { - semantics: parts.shift(), - ssrcs: parts.map(function(ssrc) { - return parseInt(ssrc, 10); - }) - }; -}; - // Extracts the MID (RFC 5888) from a media section. // returns the MID or undefined if no mid line was found. SDPUtils.getMid = function(mediaSection) { @@ -2134,7 +1810,7 @@ SDPUtils.getMid = function(mediaSection) { if (mid) { return mid.substr(6); } -}; +} SDPUtils.parseFingerprint = function(line) { var parts = line.substr(14).split(' '); @@ -2268,11 +1944,9 @@ SDPUtils.writeRtpDescription = function(kind, caps) { } sdp += 'a=rtcp-mux\r\n'; - if (caps.headerExtensions) { - caps.headerExtensions.forEach(function(extension) { - sdp += SDPUtils.writeExtmap(extension); - }); - } + caps.headerExtensions.forEach(function(extension) { + sdp += SDPUtils.writeExtmap(extension); + }); // FIXME: write fecMechanisms. return sdp; }; @@ -2298,7 +1972,8 @@ SDPUtils.parseRtpEncodingParameters = function(mediaSection) { var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID') .map(function(line) { - var parts = line.substr(17).split(' '); + var parts = line.split(' '); + parts.shift(); return parts.map(function(part) { return parseInt(part, 10); }); @@ -2311,16 +1986,16 @@ SDPUtils.parseRtpEncodingParameters = function(mediaSection) { if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) { var encParam = { ssrc: primarySsrc, - codecPayloadType: parseInt(codec.parameters.apt, 10) + codecPayloadType: parseInt(codec.parameters.apt, 10), + rtx: { + ssrc: secondarySsrc + } }; - if (primarySsrc && secondarySsrc) { - encParam.rtx = {ssrc: secondarySsrc}; - } encodingParameters.push(encParam); if (hasRed) { encParam = JSON.parse(JSON.stringify(encParam)); encParam.fec = { - ssrc: primarySsrc, + ssrc: secondarySsrc, mechanism: hasUlpfec ? 'red+ulpfec' : 'red' }; encodingParameters.push(encParam); @@ -2356,7 +2031,8 @@ SDPUtils.parseRtpEncodingParameters = function(mediaSection) { SDPUtils.parseRtcpParameters = function(mediaSection) { var rtcpParameters = {}; - // Gets the first SSRC. Note tha with RTX there might be multiple + var cname; + // Gets the first SSRC. Note that with RTX there might be multiple // SSRCs. var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') .map(function(line) { @@ -2397,8 +2073,8 @@ SDPUtils.parseMsid = function(mediaSection) { .map(function(line) { return SDPUtils.parseSsrcMedia(line); }) - .filter(function(msidParts) { - return msidParts.attribute === 'msid'; + .filter(function(parts) { + return parts.attribute === 'msid'; }); if (planB.length > 0) { parts = planB[0].value.split(' '); @@ -2418,8 +2094,7 @@ SDPUtils.generateSessionId = function() { // sessId argument is optional - if not supplied it will // be generated randomly // sessVersion is optional and defaults to 2 -// sessUser is optional and defaults to 'thisisadapterortc' -SDPUtils.writeSessionBoilerplate = function(sessId, sessVer, sessUser) { +SDPUtils.writeSessionBoilerplate = function(sessId, sessVer) { var sessionId; var version = sessVer !== undefined ? sessVer : 2; if (sessId) { @@ -2427,11 +2102,9 @@ SDPUtils.writeSessionBoilerplate = function(sessId, sessVer, sessUser) { } else { sessionId = SDPUtils.generateSessionId(); } - var user = sessUser || 'thisisadapterortc'; // FIXME: sess-id should be an NTP timestamp. return 'v=0\r\n' + - 'o=' + user + ' ' + sessionId + ' ' + version + - ' IN IP4 127.0.0.1\r\n' + + 'o=thisisadapterortc ' + sessionId + ' ' + version + ' IN IP4 127.0.0.1\r\n' + 's=-\r\n' + 't=0 0\r\n'; }; @@ -2523,43 +2196,15 @@ SDPUtils.isRejected = function(mediaSection) { SDPUtils.parseMLine = function(mediaSection) { var lines = SDPUtils.splitLines(mediaSection); - var parts = lines[0].substr(2).split(' '); - return { - kind: parts[0], - port: parseInt(parts[1], 10), - protocol: parts[2], - fmt: parts.slice(3).join(' ') - }; -}; - -SDPUtils.parseOLine = function(mediaSection) { - var line = SDPUtils.matchPrefix(mediaSection, 'o=')[0]; - var parts = line.substr(2).split(' '); + var mline = lines[0].split(' '); return { - username: parts[0], - sessionId: parts[1], - sessionVersion: parseInt(parts[2], 10), - netType: parts[3], - addressType: parts[4], - address: parts[5] + kind: mline[0].substr(2), + port: parseInt(mline[1], 10), + protocol: mline[2], + fmt: mline.slice(3).join(' ') }; }; -// a very naive interpretation of a valid SDP. -SDPUtils.isValidSDP = function(blob) { - if (typeof blob !== 'string' || blob.length === 0) { - return false; - } - var lines = SDPUtils.splitLines(blob); - for (var i = 0; i < lines.length; i++) { - if (lines[i].length < 2 || lines[i].charAt(1) !== '=') { - return false; - } - // TODO: check the modifier a bit more. - } - return true; -}; - // Expose public methods. if (typeof module === 'object') { module.exports = SDPUtils; @@ -2616,6 +2261,14 @@ module.exports = function(dependencies, opts) { var logging = utils.log; var browserDetails = utils.detectBrowser(window); + // Export to the adapter global object visible in the browser. + var adapter = { + browserDetails: browserDetails, + extractVersion: utils.extractVersion, + disableLog: utils.disableLog, + disableWarnings: utils.disableWarnings + }; + // Uncomment the line below if you want logging to occur, including logging // for the switch statement below. Can also be turned on in the browser via // adapter.disableLog(false), but then logging from the switch statement below @@ -2629,15 +2282,6 @@ module.exports = function(dependencies, opts) { var safariShim = require('./safari/safari_shim') || null; var commonShim = require('./common_shim') || null; - // Export to the adapter global object visible in the browser. - var adapter = { - browserDetails: browserDetails, - commonShim: commonShim, - extractVersion: utils.extractVersion, - disableLog: utils.disableLog, - disableWarnings: utils.disableWarnings - }; - // Shim browser if found. switch (browserDetails.browser) { case 'chrome': @@ -2658,12 +2302,8 @@ module.exports = function(dependencies, opts) { chromeShim.shimOnTrack(window); chromeShim.shimAddTrackRemoveTrack(window); chromeShim.shimGetSendersWithDtmf(window); - chromeShim.shimSenderReceiverGetStats(window); - chromeShim.fixNegotiationNeeded(window); commonShim.shimRTCIceCandidate(window); - commonShim.shimMaxMessageSize(window); - commonShim.shimSendThrowTypeError(window); break; case 'firefox': if (!firefoxShim || !firefoxShim.shimPeerConnection || @@ -2680,14 +2320,8 @@ module.exports = function(dependencies, opts) { firefoxShim.shimSourceObject(window); firefoxShim.shimPeerConnection(window); firefoxShim.shimOnTrack(window); - firefoxShim.shimRemoveStream(window); - firefoxShim.shimSenderGetStats(window); - firefoxShim.shimReceiverGetStats(window); - firefoxShim.shimRTCDataChannel(window); commonShim.shimRTCIceCandidate(window); - commonShim.shimMaxMessageSize(window); - commonShim.shimSendThrowTypeError(window); break; case 'edge': if (!edgeShim || !edgeShim.shimPeerConnection || !options.shimEdge) { @@ -2704,9 +2338,6 @@ module.exports = function(dependencies, opts) { edgeShim.shimReplaceTrack(window); // the edge shim implements the full RTCIceCandidate object. - - commonShim.shimMaxMessageSize(window); - commonShim.shimSendThrowTypeError(window); break; case 'safari': if (!safariShim || !options.shimSafari) { @@ -2719,16 +2350,14 @@ module.exports = function(dependencies, opts) { commonShim.shimCreateObjectURL(window); safariShim.shimRTCIceServerUrls(window); - safariShim.shimCreateOfferLegacy(window); safariShim.shimCallbacksAPI(window); safariShim.shimLocalStreamsAPI(window); safariShim.shimRemoteStreamsAPI(window); safariShim.shimTrackEventTransceiver(window); safariShim.shimGetUserMedia(window); + safariShim.shimCreateOfferLegacy(window); commonShim.shimRTCIceCandidate(window); - commonShim.shimMaxMessageSize(window); - commonShim.shimSendThrowTypeError(window); break; default: logging('Unsupported browser!'); @@ -2738,7 +2367,7 @@ module.exports = function(dependencies, opts) { return adapter; }; -},{"./chrome/chrome_shim":5,"./common_shim":7,"./edge/edge_shim":8,"./firefox/firefox_shim":11,"./safari/safari_shim":13,"./utils":14}],5:[function(require,module,exports){ +},{"./chrome/chrome_shim":5,"./common_shim":7,"./edge/edge_shim":8,"./firefox/firefox_shim":10,"./safari/safari_shim":12,"./utils":13}],5:[function(require,module,exports){ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. @@ -2752,49 +2381,7 @@ module.exports = function(dependencies, opts) { var utils = require('../utils.js'); var logging = utils.log; -/* iterates the stats graph recursively. */ -function walkStats(stats, base, resultSet) { - if (!base || resultSet.has(base.id)) { - return; - } - resultSet.set(base.id, base); - Object.keys(base).forEach(function(name) { - if (name.endsWith('Id')) { - walkStats(stats, stats.get(base[name]), resultSet); - } else if (name.endsWith('Ids')) { - base[name].forEach(function(id) { - walkStats(stats, stats.get(id), resultSet); - }); - } - }); -} - -/* filter getStats for a sender/receiver track. */ -function filterStats(result, track, outbound) { - var streamStatsType = outbound ? 'outbound-rtp' : 'inbound-rtp'; - var filteredResult = new Map(); - if (track === null) { - return filteredResult; - } - var trackStats = []; - result.forEach(function(value) { - if (value.type === 'track' && - value.trackIdentifier === track.id) { - trackStats.push(value); - } - }); - trackStats.forEach(function(trackStat) { - result.forEach(function(stats) { - if (stats.type === streamStatsType && stats.trackId === trackStat.id) { - walkStats(result, stats, filteredResult); - } - }); - }); - return filteredResult; -} - -module.exports = { - shimGetUserMedia: require('./getusermedia'), +var chromeShim = { shimMediaStream: function(window) { window.MediaStream = window.MediaStream || window.webkitMediaStream; }, @@ -2811,9 +2398,7 @@ module.exports = { this.removeEventListener('track', this._ontrack); } this.addEventListener('track', this._ontrack = f); - }, - enumerable: true, - configurable: true + } }); var origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription; @@ -2861,17 +2446,6 @@ module.exports = { } return origSetRemoteDescription.apply(pc, arguments); }; - } else { - // even if RTCRtpTransceiver is in window, it is only used and - // emitted in unified-plan. Unfortunately this means we need - // to unconditionally wrap the event. - utils.wrapPeerConnectionEvent(window, 'track', function(e) { - if (!e.transceiver) { - Object.defineProperty(e, 'transceiver', - {value: {receiver: e.receiver}}); - } - return e; - }); } }, @@ -2979,122 +2553,6 @@ module.exports = { } }, - shimSenderReceiverGetStats: function(window) { - if (!(typeof window === 'object' && window.RTCPeerConnection && - window.RTCRtpSender && window.RTCRtpReceiver)) { - return; - } - - // shim sender stats. - if (!('getStats' in window.RTCRtpSender.prototype)) { - var origGetSenders = window.RTCPeerConnection.prototype.getSenders; - if (origGetSenders) { - window.RTCPeerConnection.prototype.getSenders = function() { - var pc = this; - var senders = origGetSenders.apply(pc, []); - senders.forEach(function(sender) { - sender._pc = pc; - }); - return senders; - }; - } - - var origAddTrack = window.RTCPeerConnection.prototype.addTrack; - if (origAddTrack) { - window.RTCPeerConnection.prototype.addTrack = function() { - var sender = origAddTrack.apply(this, arguments); - sender._pc = this; - return sender; - }; - } - window.RTCRtpSender.prototype.getStats = function() { - var sender = this; - return this._pc.getStats().then(function(result) { - /* Note: this will include stats of all senders that - * send a track with the same id as sender.track as - * it is not possible to identify the RTCRtpSender. - */ - return filterStats(result, sender.track, true); - }); - }; - } - - // shim receiver stats. - if (!('getStats' in window.RTCRtpReceiver.prototype)) { - var origGetReceivers = window.RTCPeerConnection.prototype.getReceivers; - if (origGetReceivers) { - window.RTCPeerConnection.prototype.getReceivers = function() { - var pc = this; - var receivers = origGetReceivers.apply(pc, []); - receivers.forEach(function(receiver) { - receiver._pc = pc; - }); - return receivers; - }; - } - utils.wrapPeerConnectionEvent(window, 'track', function(e) { - e.receiver._pc = e.srcElement; - return e; - }); - window.RTCRtpReceiver.prototype.getStats = function() { - var receiver = this; - return this._pc.getStats().then(function(result) { - return filterStats(result, receiver.track, false); - }); - }; - } - - if (!('getStats' in window.RTCRtpSender.prototype && - 'getStats' in window.RTCRtpReceiver.prototype)) { - return; - } - - // shim RTCPeerConnection.getStats(track). - var origGetStats = window.RTCPeerConnection.prototype.getStats; - window.RTCPeerConnection.prototype.getStats = function() { - var pc = this; - if (arguments.length > 0 && - arguments[0] instanceof window.MediaStreamTrack) { - var track = arguments[0]; - var sender; - var receiver; - var err; - pc.getSenders().forEach(function(s) { - if (s.track === track) { - if (sender) { - err = true; - } else { - sender = s; - } - } - }); - pc.getReceivers().forEach(function(r) { - if (r.track === track) { - if (receiver) { - err = true; - } else { - receiver = r; - } - } - return r.track === track; - }); - if (err || (sender && receiver)) { - return Promise.reject(new DOMException( - 'There are more than one sender or receiver for the track.', - 'InvalidAccessError')); - } else if (sender) { - return sender.getStats(); - } else if (receiver) { - return receiver.getStats(); - } - return Promise.reject(new DOMException( - 'There is no sender or receiver for the track.', - 'InvalidAccessError')); - } - return origGetStats.apply(pc, arguments); - }; - }, - shimSourceObject: function(window) { var URL = window && window.URL; @@ -3139,88 +2597,12 @@ module.exports = { } }, - shimAddTrackRemoveTrackWithNative: function(window) { - // shim addTrack/removeTrack with native variants in order to make - // the interactions with legacy getLocalStreams behave as in other browsers. - // Keeps a mapping stream.id => [stream, rtpsenders...] - window.RTCPeerConnection.prototype.getLocalStreams = function() { - var pc = this; - this._shimmedLocalStreams = this._shimmedLocalStreams || {}; - return Object.keys(this._shimmedLocalStreams).map(function(streamId) { - return pc._shimmedLocalStreams[streamId][0]; - }); - }; - - var origAddTrack = window.RTCPeerConnection.prototype.addTrack; - window.RTCPeerConnection.prototype.addTrack = function(track, stream) { - if (!stream) { - return origAddTrack.apply(this, arguments); - } - this._shimmedLocalStreams = this._shimmedLocalStreams || {}; - - var sender = origAddTrack.apply(this, arguments); - if (!this._shimmedLocalStreams[stream.id]) { - this._shimmedLocalStreams[stream.id] = [stream, sender]; - } else if (this._shimmedLocalStreams[stream.id].indexOf(sender) === -1) { - this._shimmedLocalStreams[stream.id].push(sender); - } - return sender; - }; - - var origAddStream = window.RTCPeerConnection.prototype.addStream; - window.RTCPeerConnection.prototype.addStream = function(stream) { - var pc = this; - this._shimmedLocalStreams = this._shimmedLocalStreams || {}; - - stream.getTracks().forEach(function(track) { - var alreadyExists = pc.getSenders().find(function(s) { - return s.track === track; - }); - if (alreadyExists) { - throw new DOMException('Track already exists.', - 'InvalidAccessError'); - } - }); - var existingSenders = pc.getSenders(); - origAddStream.apply(this, arguments); - var newSenders = pc.getSenders().filter(function(newSender) { - return existingSenders.indexOf(newSender) === -1; - }); - this._shimmedLocalStreams[stream.id] = [stream].concat(newSenders); - }; - - var origRemoveStream = window.RTCPeerConnection.prototype.removeStream; - window.RTCPeerConnection.prototype.removeStream = function(stream) { - this._shimmedLocalStreams = this._shimmedLocalStreams || {}; - delete this._shimmedLocalStreams[stream.id]; - return origRemoveStream.apply(this, arguments); - }; - - var origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack; - window.RTCPeerConnection.prototype.removeTrack = function(sender) { - var pc = this; - this._shimmedLocalStreams = this._shimmedLocalStreams || {}; - if (sender) { - Object.keys(this._shimmedLocalStreams).forEach(function(streamId) { - var idx = pc._shimmedLocalStreams[streamId].indexOf(sender); - if (idx !== -1) { - pc._shimmedLocalStreams[streamId].splice(idx, 1); - } - if (pc._shimmedLocalStreams[streamId].length === 1) { - delete pc._shimmedLocalStreams[streamId]; - } - }); - } - return origRemoveTrack.apply(this, arguments); - }; - }, - shimAddTrackRemoveTrack: function(window) { var browserDetails = utils.detectBrowser(window); // shim addTrack and removeTrack. if (window.RTCPeerConnection.prototype.addTrack && - browserDetails.version >= 65) { - return this.shimAddTrackRemoveTrackWithNative(window); + browserDetails.version >= 63) { + return; } // also shim pc.getLocalStreams when addTrack is shimmed @@ -3228,11 +2610,11 @@ module.exports = { var origGetLocalStreams = window.RTCPeerConnection.prototype .getLocalStreams; window.RTCPeerConnection.prototype.getLocalStreams = function() { - var pc = this; + var self = this; var nativeStreams = origGetLocalStreams.apply(this); - pc._reverseStreams = pc._reverseStreams || {}; + self._reverseStreams = self._reverseStreams || {}; return nativeStreams.map(function(stream) { - return pc._reverseStreams[stream.id]; + return self._reverseStreams[stream.id]; }); }; @@ -3458,7 +2840,7 @@ module.exports = { var browserDetails = utils.detectBrowser(window); // The RTCPeerConnection object. - if (!window.RTCPeerConnection && window.webkitRTCPeerConnection) { + if (!window.RTCPeerConnection) { window.RTCPeerConnection = function(pcConfig, pcConstraints) { // Translate iceTransportPolicy to iceTransports, // see https://code.google.com/p/webrtc/issues/detail?id=4869 @@ -3480,12 +2862,41 @@ module.exports = { } }); } + } else { + // migrate from non-spec RTCIceServer.url to RTCIceServer.urls + var OrigPeerConnection = window.RTCPeerConnection; + window.RTCPeerConnection = function(pcConfig, pcConstraints) { + if (pcConfig && pcConfig.iceServers) { + var newIceServers = []; + for (var i = 0; i < pcConfig.iceServers.length; i++) { + var server = pcConfig.iceServers[i]; + if (!server.hasOwnProperty('urls') && + server.hasOwnProperty('url')) { + utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls'); + server = JSON.parse(JSON.stringify(server)); + server.urls = server.url; + newIceServers.push(server); + } else { + newIceServers.push(pcConfig.iceServers[i]); + } + } + pcConfig.iceServers = newIceServers; + } + return new OrigPeerConnection(pcConfig, pcConstraints); + }; + window.RTCPeerConnection.prototype = OrigPeerConnection.prototype; + // wrap static methods. Currently just generateCertificate. + Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { + get: function() { + return OrigPeerConnection.generateCertificate; + } + }); } var origGetStats = window.RTCPeerConnection.prototype.getStats; window.RTCPeerConnection.prototype.getStats = function(selector, successCallback, errorCallback) { - var pc = this; + var self = this; var args = arguments; // If selector is a function then we are in the old style stats so just @@ -3540,7 +2951,7 @@ module.exports = { // promise-support return new Promise(function(resolve, reject) { - origGetStats.apply(pc, [ + origGetStats.apply(self, [ function(response) { resolve(makeMapStats(fixChromeStats_(response))); }, reject]); @@ -3554,9 +2965,9 @@ module.exports = { var nativeMethod = window.RTCPeerConnection.prototype[method]; window.RTCPeerConnection.prototype[method] = function() { var args = arguments; - var pc = this; + var self = this; var promise = new Promise(function(resolve, reject) { - nativeMethod.apply(pc, [args[0], resolve, reject]); + nativeMethod.apply(self, [args[0], resolve, reject]); }); if (args.length < 2) { return promise; @@ -3579,12 +2990,12 @@ module.exports = { ['createOffer', 'createAnswer'].forEach(function(method) { var nativeMethod = window.RTCPeerConnection.prototype[method]; window.RTCPeerConnection.prototype[method] = function() { - var pc = this; + var self = this; if (arguments.length < 1 || (arguments.length === 1 && typeof arguments[0] === 'object')) { var opts = arguments.length === 1 ? arguments[0] : undefined; return new Promise(function(resolve, reject) { - nativeMethod.apply(pc, [resolve, reject, opts]); + nativeMethod.apply(self, [resolve, reject, opts]); }); } return nativeMethod.apply(this, arguments); @@ -3616,46 +3027,22 @@ module.exports = { } return nativeAddIceCandidate.apply(this, arguments); }; - }, + } +}; - fixNegotiationNeeded: function(window) { - utils.wrapPeerConnectionEvent(window, 'negotiationneeded', function(e) { - var pc = e.target; - if (pc.signalingState !== 'stable') { - return; - } - return e; - }); - }, - shimGetDisplayMedia: function(window, getSourceId) { - if ('getDisplayMedia' in window.navigator) { - return; - } - // getSourceId is a function that returns a promise resolving with - // the sourceId of the screen/window/tab to be shared. - if (typeof getSourceId !== 'function') { - console.error('shimGetDisplayMedia: getSourceId argument is not ' + - 'a function'); - return; - } - navigator.getDisplayMedia = function(constraints) { - return getSourceId(constraints) - .then(function(sourceId) { - constraints.video = { - mandatory: { - chromeMediaSource: 'desktop', - chromeMediaSourceId: sourceId, - maxFrameRate: constraints.video.frameRate || 3 - } - }; - return navigator.mediaDevices.getUserMedia(constraints); - }); - }; - } +// Expose public methods. +module.exports = { + shimMediaStream: chromeShim.shimMediaStream, + shimOnTrack: chromeShim.shimOnTrack, + shimAddTrackRemoveTrack: chromeShim.shimAddTrackRemoveTrack, + shimGetSendersWithDtmf: chromeShim.shimGetSendersWithDtmf, + shimSourceObject: chromeShim.shimSourceObject, + shimPeerConnection: chromeShim.shimPeerConnection, + shimGetUserMedia: require('./getusermedia') }; -},{"../utils.js":14,"./getusermedia":6}],6:[function(require,module,exports){ +},{"../utils.js":13,"./getusermedia":6}],6:[function(require,module,exports){ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * @@ -3725,9 +3112,6 @@ module.exports = function(window) { }; var shimConstraints_ = function(constraints, func) { - if (browserDetails.version >= 61) { - return func(constraints); - } constraints = JSON.parse(JSON.stringify(constraints)); if (constraints && typeof constraints.audio === 'object') { var remap = function(obj, a, b) { @@ -3791,25 +3175,18 @@ module.exports = function(window) { }; var shimError_ = function(e) { - if (browserDetails.version >= 64) { - return e; - } return { name: { PermissionDeniedError: 'NotAllowedError', - PermissionDismissedError: 'NotAllowedError', - InvalidStateError: 'NotAllowedError', + InvalidStateError: 'NotReadableError', DevicesNotFoundError: 'NotFoundError', ConstraintNotSatisfiedError: 'OverconstrainedError', TrackStartError: 'NotReadableError', - MediaDeviceFailedDueToShutdown: 'NotAllowedError', - MediaDeviceKillSwitchOn: 'NotAllowedError', - TabCaptureError: 'AbortError', - ScreenCaptureError: 'AbortError', - DeviceCaptureError: 'AbortError' + MediaDeviceFailedDueToShutdown: 'NotReadableError', + MediaDeviceKillSwitchOn: 'NotReadableError' }[e.name] || e.name, message: e.message, - constraint: e.constraint || e.constraintName, + constraint: e.constraintName, toString: function() { return this.name + (this.message && ': ') + this.message; } @@ -3904,7 +3281,7 @@ module.exports = function(window) { } }; -},{"../utils.js":14}],7:[function(require,module,exports){ +},{"../utils.js":13}],7:[function(require,module,exports){ /* * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. * @@ -3918,12 +3295,63 @@ module.exports = function(window) { var SDPUtils = require('sdp'); var utils = require('./utils'); +// Wraps the peerconnection event eventNameToWrap in a function +// which returns the modified event object. +function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) { + if (!window.RTCPeerConnection) { + return; + } + var proto = window.RTCPeerConnection.prototype; + var nativeAddEventListener = proto.addEventListener; + proto.addEventListener = function(nativeEventName, cb) { + if (nativeEventName !== eventNameToWrap) { + return nativeAddEventListener.apply(this, arguments); + } + var wrappedCallback = function(e) { + cb(wrapper(e)); + }; + this._eventMap = this._eventMap || {}; + this._eventMap[cb] = wrappedCallback; + return nativeAddEventListener.apply(this, [nativeEventName, + wrappedCallback]); + }; + + var nativeRemoveEventListener = proto.removeEventListener; + proto.removeEventListener = function(nativeEventName, cb) { + if (nativeEventName !== eventNameToWrap || !this._eventMap + || !this._eventMap[cb]) { + return nativeRemoveEventListener.apply(this, arguments); + } + var unwrappedCb = this._eventMap[cb]; + delete this._eventMap[cb]; + return nativeRemoveEventListener.apply(this, [nativeEventName, + unwrappedCb]); + }; + + Object.defineProperty(proto, 'on' + eventNameToWrap, { + get: function() { + return this['_on' + eventNameToWrap]; + }, + set: function(cb) { + if (this['_on' + eventNameToWrap]) { + this.removeEventListener(eventNameToWrap, + this['_on' + eventNameToWrap]); + delete this['_on' + eventNameToWrap]; + } + if (cb) { + this.addEventListener(eventNameToWrap, + this['_on' + eventNameToWrap] = cb); + } + } + }); +} + module.exports = { shimRTCIceCandidate: function(window) { // foundation is arbitrarily chosen as an indicator for full support for // https://w3c.github.io/webrtc-pc/#rtcicecandidate-interface - if (!window.RTCIceCandidate || (window.RTCIceCandidate && 'foundation' in - window.RTCIceCandidate.prototype)) { + if (window.RTCIceCandidate && 'foundation' in + window.RTCIceCandidate.prototype) { return; } @@ -3936,31 +3364,27 @@ module.exports = { args.candidate = args.candidate.substr(2); } - if (args.candidate && args.candidate.length) { - // Augment the native candidate with the parsed fields. - var nativeCandidate = new NativeRTCIceCandidate(args); - var parsedCandidate = SDPUtils.parseCandidate(args.candidate); - var augmentedCandidate = Object.assign(nativeCandidate, - parsedCandidate); - - // Add a serializer that does not serialize the extra attributes. - augmentedCandidate.toJSON = function() { - return { - candidate: augmentedCandidate.candidate, - sdpMid: augmentedCandidate.sdpMid, - sdpMLineIndex: augmentedCandidate.sdpMLineIndex, - usernameFragment: augmentedCandidate.usernameFragment, - }; + // Augment the native candidate with the parsed fields. + var nativeCandidate = new NativeRTCIceCandidate(args); + var parsedCandidate = SDPUtils.parseCandidate(args.candidate); + var augmentedCandidate = Object.assign(nativeCandidate, + parsedCandidate); + + // Add a serializer that does not serialize the extra attributes. + augmentedCandidate.toJSON = function() { + return { + candidate: augmentedCandidate.candidate, + sdpMid: augmentedCandidate.sdpMid, + sdpMLineIndex: augmentedCandidate.sdpMLineIndex, + usernameFragment: augmentedCandidate.usernameFragment, }; - return augmentedCandidate; - } - return new NativeRTCIceCandidate(args); + }; + return augmentedCandidate; }; - window.RTCIceCandidate.prototype = NativeRTCIceCandidate.prototype; // Hook up the augmented candidate in onicecandidate and // addEventListener('icecandidate', ...) - utils.wrapPeerConnectionEvent(window, 'icecandidate', function(e) { + wrapPeerConnectionEvent(window, 'icecandidate', function(e) { if (e.candidate) { Object.defineProperty(e, 'candidate', { value: new window.RTCIceCandidate(e.candidate), @@ -4022,181 +3446,10 @@ module.exports = { } return nativeSetAttribute.apply(this, arguments); }; - }, + } +}; - shimMaxMessageSize: function(window) { - if (window.RTCSctpTransport || !window.RTCPeerConnection) { - return; - } - var browserDetails = utils.detectBrowser(window); - - if (!('sctp' in window.RTCPeerConnection.prototype)) { - Object.defineProperty(window.RTCPeerConnection.prototype, 'sctp', { - get: function() { - return typeof this._sctp === 'undefined' ? null : this._sctp; - } - }); - } - - var sctpInDescription = function(description) { - var sections = SDPUtils.splitSections(description.sdp); - sections.shift(); - return sections.some(function(mediaSection) { - var mLine = SDPUtils.parseMLine(mediaSection); - return mLine && mLine.kind === 'application' - && mLine.protocol.indexOf('SCTP') !== -1; - }); - }; - - var getRemoteFirefoxVersion = function(description) { - // TODO: Is there a better solution for detecting Firefox? - var match = description.sdp.match(/mozilla...THIS_IS_SDPARTA-(\d+)/); - if (match === null || match.length < 2) { - return -1; - } - var version = parseInt(match[1], 10); - // Test for NaN (yes, this is ugly) - return version !== version ? -1 : version; - }; - - var getCanSendMaxMessageSize = function(remoteIsFirefox) { - // Every implementation we know can send at least 64 KiB. - // Note: Although Chrome is technically able to send up to 256 KiB, the - // data does not reach the other peer reliably. - // See: https://bugs.chromium.org/p/webrtc/issues/detail?id=8419 - var canSendMaxMessageSize = 65536; - if (browserDetails.browser === 'firefox') { - if (browserDetails.version < 57) { - if (remoteIsFirefox === -1) { - // FF < 57 will send in 16 KiB chunks using the deprecated PPID - // fragmentation. - canSendMaxMessageSize = 16384; - } else { - // However, other FF (and RAWRTC) can reassemble PPID-fragmented - // messages. Thus, supporting ~2 GiB when sending. - canSendMaxMessageSize = 2147483637; - } - } else if (browserDetails.version < 60) { - // Currently, all FF >= 57 will reset the remote maximum message size - // to the default value when a data channel is created at a later - // stage. :( - // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1426831 - canSendMaxMessageSize = - browserDetails.version === 57 ? 65535 : 65536; - } else { - // FF >= 60 supports sending ~2 GiB - canSendMaxMessageSize = 2147483637; - } - } - return canSendMaxMessageSize; - }; - - var getMaxMessageSize = function(description, remoteIsFirefox) { - // Note: 65536 bytes is the default value from the SDP spec. Also, - // every implementation we know supports receiving 65536 bytes. - var maxMessageSize = 65536; - - // FF 57 has a slightly incorrect default remote max message size, so - // we need to adjust it here to avoid a failure when sending. - // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1425697 - if (browserDetails.browser === 'firefox' - && browserDetails.version === 57) { - maxMessageSize = 65535; - } - - var match = SDPUtils.matchPrefix(description.sdp, 'a=max-message-size:'); - if (match.length > 0) { - maxMessageSize = parseInt(match[0].substr(19), 10); - } else if (browserDetails.browser === 'firefox' && - remoteIsFirefox !== -1) { - // If the maximum message size is not present in the remote SDP and - // both local and remote are Firefox, the remote peer can receive - // ~2 GiB. - maxMessageSize = 2147483637; - } - return maxMessageSize; - }; - - var origSetRemoteDescription = - window.RTCPeerConnection.prototype.setRemoteDescription; - window.RTCPeerConnection.prototype.setRemoteDescription = function() { - var pc = this; - pc._sctp = null; - - if (sctpInDescription(arguments[0])) { - // Check if the remote is FF. - var isFirefox = getRemoteFirefoxVersion(arguments[0]); - - // Get the maximum message size the local peer is capable of sending - var canSendMMS = getCanSendMaxMessageSize(isFirefox); - - // Get the maximum message size of the remote peer. - var remoteMMS = getMaxMessageSize(arguments[0], isFirefox); - - // Determine final maximum message size - var maxMessageSize; - if (canSendMMS === 0 && remoteMMS === 0) { - maxMessageSize = Number.POSITIVE_INFINITY; - } else if (canSendMMS === 0 || remoteMMS === 0) { - maxMessageSize = Math.max(canSendMMS, remoteMMS); - } else { - maxMessageSize = Math.min(canSendMMS, remoteMMS); - } - - // Create a dummy RTCSctpTransport object and the 'maxMessageSize' - // attribute. - var sctp = {}; - Object.defineProperty(sctp, 'maxMessageSize', { - get: function() { - return maxMessageSize; - } - }); - pc._sctp = sctp; - } - - return origSetRemoteDescription.apply(pc, arguments); - }; - }, - - shimSendThrowTypeError: function(window) { - if (!(window.RTCPeerConnection && - 'createDataChannel' in window.RTCPeerConnection.prototype)) { - return; - } - - // Note: Although Firefox >= 57 has a native implementation, the maximum - // message size can be reset for all data channels at a later stage. - // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1426831 - - function wrapDcSend(dc, pc) { - var origDataChannelSend = dc.send; - dc.send = function() { - var data = arguments[0]; - var length = data.length || data.size || data.byteLength; - if (dc.readyState === 'open' && - pc.sctp && length > pc.sctp.maxMessageSize) { - throw new TypeError('Message too large (can send a maximum of ' + - pc.sctp.maxMessageSize + ' bytes)'); - } - return origDataChannelSend.apply(dc, arguments); - }; - } - var origCreateDataChannel = - window.RTCPeerConnection.prototype.createDataChannel; - window.RTCPeerConnection.prototype.createDataChannel = function() { - var pc = this; - var dataChannel = origCreateDataChannel.apply(pc, arguments); - wrapDcSend(dataChannel, pc); - return dataChannel; - }; - utils.wrapPeerConnectionEvent(window, 'datachannel', function(e) { - wrapDcSend(e.channel, e.target); - return e; - }); - } -}; - -},{"./utils":14,"sdp":2}],8:[function(require,module,exports){ +},{"./utils":13,"sdp":2}],8:[function(require,module,exports){ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * @@ -4208,7 +3461,6 @@ module.exports = { 'use strict'; var utils = require('../utils'); -var filterIceServers = require('./filtericeservers'); var shimRTCPeerConnection = require('rtcpeerconnection-shim'); module.exports = { @@ -4217,11 +3469,16 @@ module.exports = { var browserDetails = utils.detectBrowser(window); if (window.RTCIceGatherer) { + // ORTC defines an RTCIceCandidate object but no constructor. + // Not implemented in Edge. if (!window.RTCIceCandidate) { window.RTCIceCandidate = function(args) { return args; }; } + // ORTC does not have a session description object but + // other browsers (i.e. Chrome) that will support both PC and ORTC + // in the future might have this defined already. if (!window.RTCSessionDescription) { window.RTCSessionDescription = function(args) { return args; @@ -4260,21 +3517,9 @@ module.exports = { } }); } - // Edge currently only implements the RTCDtmfSender, not the - // RTCDTMFSender alias. See http://draft.ortc.org/#rtcdtmfsender2* - if (window.RTCDtmfSender && !window.RTCDTMFSender) { - window.RTCDTMFSender = window.RTCDtmfSender; - } - var RTCPeerConnectionShim = shimRTCPeerConnection(window, - browserDetails.version); - window.RTCPeerConnection = function(config) { - if (config && config.iceServers) { - config.iceServers = filterIceServers(config.iceServers); - } - return new RTCPeerConnectionShim(config); - }; - window.RTCPeerConnection.prototype = RTCPeerConnectionShim.prototype; + window.RTCPeerConnection = + shimRTCPeerConnection(window, browserDetails.version); }, shimReplaceTrack: function(window) { // ORTC has replaceTrack -- https://github.com/w3c/ortc/issues/614 @@ -4286,58 +3531,7 @@ module.exports = { } }; -},{"../utils":14,"./filtericeservers":9,"./getusermedia":10,"rtcpeerconnection-shim":1}],9:[function(require,module,exports){ -/* - * Copyright (c) 2018 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. - */ - /* eslint-env node */ -'use strict'; - -var utils = require('../utils'); -// Edge does not like -// 1) stun: filtered after 14393 unless ?transport=udp is present -// 2) turn: that does not have all of turn:host:port?transport=udp -// 3) turn: with ipv6 addresses -// 4) turn: occurring muliple times -module.exports = function(iceServers, edgeVersion) { - var hasTurn = false; - iceServers = JSON.parse(JSON.stringify(iceServers)); - return iceServers.filter(function(server) { - if (server && (server.urls || server.url)) { - var urls = server.urls || server.url; - if (server.url && !server.urls) { - utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls'); - } - var isString = typeof urls === 'string'; - if (isString) { - urls = [urls]; - } - urls = urls.filter(function(url) { - var validTurn = url.indexOf('turn:') === 0 && - url.indexOf('transport=udp') !== -1 && - url.indexOf('turn:[') === -1 && - !hasTurn; - - if (validTurn) { - hasTurn = true; - return true; - } - return url.indexOf('stun:') === 0 && edgeVersion >= 14393 && - url.indexOf('?transport=udp') === -1; - }); - - delete server.url; - server.urls = isString ? urls[0] : urls; - return !!urls.length; - } - }); -}; - -},{"../utils":14}],10:[function(require,module,exports){ +},{"../utils":13,"./getusermedia":9,"rtcpeerconnection-shim":1}],9:[function(require,module,exports){ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * @@ -4373,7 +3567,7 @@ module.exports = function(window) { }; }; -},{}],11:[function(require,module,exports){ +},{}],10:[function(require,module,exports){ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * @@ -4386,8 +3580,7 @@ module.exports = function(window) { var utils = require('../utils'); -module.exports = { - shimGetUserMedia: require('./getusermedia'), +var firefoxShim = { shimOnTrack: function(window) { if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in window.RTCPeerConnection.prototype)) { @@ -4411,9 +3604,7 @@ module.exports = { this.dispatchEvent(event); }.bind(this)); }.bind(this)); - }, - enumerable: true, - configurable: true + } }); } if (typeof window === 'object' && window.RTCTrackEvent && @@ -4575,118 +3766,18 @@ module.exports = { }) .then(onSucc, onErr); }; - }, - - shimSenderGetStats: function(window) { - if (!(typeof window === 'object' && window.RTCPeerConnection && - window.RTCRtpSender)) { - return; - } - if (window.RTCRtpSender && 'getStats' in window.RTCRtpSender.prototype) { - return; - } - var origGetSenders = window.RTCPeerConnection.prototype.getSenders; - if (origGetSenders) { - window.RTCPeerConnection.prototype.getSenders = function() { - var pc = this; - var senders = origGetSenders.apply(pc, []); - senders.forEach(function(sender) { - sender._pc = pc; - }); - return senders; - }; - } - - var origAddTrack = window.RTCPeerConnection.prototype.addTrack; - if (origAddTrack) { - window.RTCPeerConnection.prototype.addTrack = function() { - var sender = origAddTrack.apply(this, arguments); - sender._pc = this; - return sender; - }; - } - window.RTCRtpSender.prototype.getStats = function() { - return this.track ? this._pc.getStats(this.track) : - Promise.resolve(new Map()); - }; - }, - - shimReceiverGetStats: function(window) { - if (!(typeof window === 'object' && window.RTCPeerConnection && - window.RTCRtpSender)) { - return; - } - if (window.RTCRtpSender && 'getStats' in window.RTCRtpReceiver.prototype) { - return; - } - var origGetReceivers = window.RTCPeerConnection.prototype.getReceivers; - if (origGetReceivers) { - window.RTCPeerConnection.prototype.getReceivers = function() { - var pc = this; - var receivers = origGetReceivers.apply(pc, []); - receivers.forEach(function(receiver) { - receiver._pc = pc; - }); - return receivers; - }; - } - utils.wrapPeerConnectionEvent(window, 'track', function(e) { - e.receiver._pc = e.srcElement; - return e; - }); - window.RTCRtpReceiver.prototype.getStats = function() { - return this._pc.getStats(this.track); - }; - }, - - shimRemoveStream: function(window) { - if (!window.RTCPeerConnection || - 'removeStream' in window.RTCPeerConnection.prototype) { - return; - } - window.RTCPeerConnection.prototype.removeStream = function(stream) { - var pc = this; - utils.deprecated('removeStream', 'removeTrack'); - this.getSenders().forEach(function(sender) { - if (sender.track && stream.getTracks().indexOf(sender.track) !== -1) { - pc.removeTrack(sender); - } - }); - }; - }, - - shimRTCDataChannel: function(window) { - // rename DataChannel to RTCDataChannel (native fix in FF60): - // https://bugzilla.mozilla.org/show_bug.cgi?id=1173851 - if (window.DataChannel && !window.RTCDataChannel) { - window.RTCDataChannel = window.DataChannel; - } - }, - - shimGetDisplayMedia: function(window, preferredMediaSource) { - if ('getDisplayMedia' in window.navigator) { - return; - } - navigator.getDisplayMedia = function(constraints) { - if (!(constraints && constraints.video)) { - var err = new DOMException('getDisplayMedia without video ' + - 'constraints is undefined'); - err.name = 'NotFoundError'; - // from https://heycam.github.io/webidl/#idl-DOMException-error-names - err.code = 8; - return Promise.reject(err); - } - if (constraints.video === true) { - constraints.video = {mediaSource: preferredMediaSource}; - } else { - constraints.video.mediaSource = preferredMediaSource; - } - return navigator.mediaDevices.getUserMedia(constraints); - }; } }; -},{"../utils":14,"./getusermedia":12}],12:[function(require,module,exports){ +// Expose public methods. +module.exports = { + shimOnTrack: firefoxShim.shimOnTrack, + shimSourceObject: firefoxShim.shimSourceObject, + shimPeerConnection: firefoxShim.shimPeerConnection, + shimGetUserMedia: require('./getusermedia') +}; + +},{"../utils":13,"./getusermedia":11}],11:[function(require,module,exports){ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * @@ -4897,7 +3988,7 @@ module.exports = function(window) { }; }; -},{"../utils":14}],13:[function(require,module,exports){ +},{"../utils":13}],12:[function(require,module,exports){ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * @@ -4908,7 +3999,13 @@ module.exports = function(window) { 'use strict'; var utils = require('../utils'); -module.exports = { +var safariShim = { + // TODO: DrAlex, should be here, double check against LayoutTests + + // TODO: once the back-end for the mac port is done, add. + // TODO: check for webkitGTK+ + // shimPeerConnection: function() { }, + shimLocalStreamsAPI: function(window) { if (typeof window !== 'object' || !window.RTCPeerConnection) { return; @@ -4950,9 +4047,9 @@ module.exports = { if (this._localStreams.indexOf(stream) === -1) { this._localStreams.push(stream); } - var pc = this; + var self = this; stream.getTracks().forEach(function(track) { - _addTrack.call(pc, track, stream); + _addTrack.call(self, track, stream); }); }; @@ -4964,7 +4061,7 @@ module.exports = { this._localStreams.push(stream); } } - return _addTrack.call(this, track, stream); + _addTrack.call(this, track, stream); }; } if (!('removeStream' in window.RTCPeerConnection.prototype)) { @@ -4977,11 +4074,11 @@ module.exports = { return; } this._localStreams.splice(index, 1); - var pc = this; + var self = this; var tracks = stream.getTracks(); this.getSenders().forEach(function(sender) { if (tracks.indexOf(sender.track) !== -1) { - pc.removeTrack(sender); + self.removeTrack(sender); } }); }; @@ -5004,32 +4101,24 @@ module.exports = { set: function(f) { if (this._onaddstream) { this.removeEventListener('addstream', this._onaddstream); + this.removeEventListener('track', this._onaddstreampoly); } this.addEventListener('addstream', this._onaddstream = f); - } - }); - var origSetRemoteDescription = - window.RTCPeerConnection.prototype.setRemoteDescription; - window.RTCPeerConnection.prototype.setRemoteDescription = function() { - var pc = this; - if (!this._onaddstreampoly) { this.addEventListener('track', this._onaddstreampoly = function(e) { - e.streams.forEach(function(stream) { - if (!pc._remoteStreams) { - pc._remoteStreams = []; - } - if (pc._remoteStreams.indexOf(stream) >= 0) { - return; - } - pc._remoteStreams.push(stream); - var event = new Event('addstream'); - event.stream = stream; - pc.dispatchEvent(event); - }); - }); + var stream = e.streams[0]; + if (!this._remoteStreams) { + this._remoteStreams = []; + } + if (this._remoteStreams.indexOf(stream) >= 0) { + return; + } + this._remoteStreams.push(stream); + var event = new Event('addstream'); + event.stream = e.streams[0]; + this.dispatchEvent(event); + }.bind(this)); } - return origSetRemoteDescription.apply(pc, arguments); - }; + }); } }, shimCallbacksAPI: function(window) { @@ -5161,38 +4250,21 @@ module.exports = { window.RTCPeerConnection.prototype.createOffer = function(offerOptions) { var pc = this; if (offerOptions) { - if (typeof offerOptions.offerToReceiveAudio !== 'undefined') { - // support bit values - offerOptions.offerToReceiveAudio = !!offerOptions.offerToReceiveAudio; - } var audioTransceiver = pc.getTransceivers().find(function(transceiver) { return transceiver.sender.track && transceiver.sender.track.kind === 'audio'; }); if (offerOptions.offerToReceiveAudio === false && audioTransceiver) { if (audioTransceiver.direction === 'sendrecv') { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection('sendonly'); - } else { - audioTransceiver.direction = 'sendonly'; - } + audioTransceiver.setDirection('sendonly'); } else if (audioTransceiver.direction === 'recvonly') { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection('inactive'); - } else { - audioTransceiver.direction = 'inactive'; - } + audioTransceiver.setDirection('inactive'); } } else if (offerOptions.offerToReceiveAudio === true && !audioTransceiver) { pc.addTransceiver('audio'); } - - if (typeof offerOptions.offerToReceiveVideo !== 'undefined') { - // support bit values - offerOptions.offerToReceiveVideo = !!offerOptions.offerToReceiveVideo; - } var videoTransceiver = pc.getTransceivers().find(function(transceiver) { return transceiver.sender.track && transceiver.sender.track.kind === 'video'; @@ -5213,7 +4285,20 @@ module.exports = { } }; -},{"../utils":14}],14:[function(require,module,exports){ +// Expose public methods. +module.exports = { + shimCallbacksAPI: safariShim.shimCallbacksAPI, + shimLocalStreamsAPI: safariShim.shimLocalStreamsAPI, + shimRemoteStreamsAPI: safariShim.shimRemoteStreamsAPI, + shimGetUserMedia: safariShim.shimGetUserMedia, + shimRTCIceServerUrls: safariShim.shimRTCIceServerUrls, + shimTrackEventTransceiver: safariShim.shimTrackEventTransceiver, + shimCreateOfferLegacy: safariShim.shimCreateOfferLegacy + // TODO + // shimPeerConnection: safariShim.shimPeerConnection +}; + +},{"../utils":13}],13:[function(require,module,exports){ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * @@ -5227,80 +4312,8 @@ module.exports = { var logDisabled_ = true; var deprecationWarnings_ = true; -/** - * Extract browser version out of the provided user agent string. - * - * @param {!string} uastring userAgent string. - * @param {!string} expr Regular expression used as match criteria. - * @param {!number} pos position in the version string to be returned. - * @return {!number} browser version. - */ -function extractVersion(uastring, expr, pos) { - var match = uastring.match(expr); - return match && match.length >= pos && parseInt(match[pos], 10); -} - -// Wraps the peerconnection event eventNameToWrap in a function -// which returns the modified event object (or false to prevent -// the event). -function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) { - if (!window.RTCPeerConnection) { - return; - } - var proto = window.RTCPeerConnection.prototype; - var nativeAddEventListener = proto.addEventListener; - proto.addEventListener = function(nativeEventName, cb) { - if (nativeEventName !== eventNameToWrap) { - return nativeAddEventListener.apply(this, arguments); - } - var wrappedCallback = function(e) { - var modifiedEvent = wrapper(e); - if (modifiedEvent) { - cb(modifiedEvent); - } - }; - this._eventMap = this._eventMap || {}; - this._eventMap[cb] = wrappedCallback; - return nativeAddEventListener.apply(this, [nativeEventName, - wrappedCallback]); - }; - - var nativeRemoveEventListener = proto.removeEventListener; - proto.removeEventListener = function(nativeEventName, cb) { - if (nativeEventName !== eventNameToWrap || !this._eventMap - || !this._eventMap[cb]) { - return nativeRemoveEventListener.apply(this, arguments); - } - var unwrappedCb = this._eventMap[cb]; - delete this._eventMap[cb]; - return nativeRemoveEventListener.apply(this, [nativeEventName, - unwrappedCb]); - }; - - Object.defineProperty(proto, 'on' + eventNameToWrap, { - get: function() { - return this['_on' + eventNameToWrap]; - }, - set: function(cb) { - if (this['_on' + eventNameToWrap]) { - this.removeEventListener(eventNameToWrap, - this['_on' + eventNameToWrap]); - delete this['_on' + eventNameToWrap]; - } - if (cb) { - this.addEventListener(eventNameToWrap, - this['_on' + eventNameToWrap] = cb); - } - }, - enumerable: true, - configurable: true - }); -} - // Utility methods. -module.exports = { - extractVersion: extractVersion, - wrapPeerConnectionEvent: wrapPeerConnectionEvent, +var utils = { disableLog: function(bool) { if (typeof bool !== 'boolean') { return new Error('Argument type: ' + typeof bool + @@ -5346,6 +4359,19 @@ module.exports = { ' instead.'); }, + /** + * Extract browser version out of the provided user agent string. + * + * @param {!string} uastring userAgent string. + * @param {!string} expr Regular expression used as match criteria. + * @param {!number} pos position in the version string to be returned. + * @return {!number} browser version. + */ + extractVersion: function(uastring, expr, pos) { + var match = uastring.match(expr); + return match && match.length >= pos && parseInt(match[pos], 10); + }, + /** * Browser detector. * @@ -5366,25 +4392,38 @@ module.exports = { return result; } - if (navigator.mozGetUserMedia) { // Firefox. + // Firefox. + if (navigator.mozGetUserMedia) { result.browser = 'firefox'; - result.version = extractVersion(navigator.userAgent, + result.version = this.extractVersion(navigator.userAgent, /Firefox\/(\d+)\./, 1); } else if (navigator.webkitGetUserMedia) { - // Chrome, Chromium, Webview, Opera. - // Version matches Chrome/WebRTC version. - result.browser = 'chrome'; - result.version = extractVersion(navigator.userAgent, + // Chrome, Chromium, Webview, Opera, all use the chrome shim for now + if (window.webkitRTCPeerConnection) { + result.browser = 'chrome'; + result.version = this.extractVersion(navigator.userAgent, /Chrom(e|ium)\/(\d+)\./, 2); + } else { // Safari (in an unpublished version) or unknown webkit-based. + if (navigator.userAgent.match(/Version\/(\d+).(\d+)/)) { + result.browser = 'safari'; + result.version = this.extractVersion(navigator.userAgent, + /AppleWebKit\/(\d+)\./, 1); + } else { // unknown webkit-based browser. + result.browser = 'Unsupported webkit-based browser ' + + 'with GUM support but no WebRTC support.'; + return result; + } + } } else if (navigator.mediaDevices && navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) { // Edge. result.browser = 'edge'; - result.version = extractVersion(navigator.userAgent, + result.version = this.extractVersion(navigator.userAgent, /Edge\/(\d+).(\d+)$/, 2); - } else if (window.RTCPeerConnection && - navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) { // Safari. + } else if (navigator.mediaDevices && + navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) { + // Safari, with webkitGetUserMedia removed. result.browser = 'safari'; - result.version = extractVersion(navigator.userAgent, + result.version = this.extractVersion(navigator.userAgent, /AppleWebKit\/(\d+)\./, 1); } else { // Default fallthrough: not supported. result.browser = 'Not a supported browser.'; @@ -5392,8 +4431,20 @@ module.exports = { } return result; - } + }, + +}; + +// Export. +module.exports = { + log: utils.log, + deprecated: utils.deprecated, + disableLog: utils.disableLog, + disableWarnings: utils.disableWarnings, + extractVersion: utils.extractVersion, + shimCreateObjectURL: utils.shimCreateObjectURL, + detectBrowser: utils.detectBrowser.bind(utils) }; },{}]},{},[3])(3) -}); +}); \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/core/UsersUtil.as b/bigbluebutton-client/src/org/bigbluebutton/core/UsersUtil.as index 0dc3d7780bfd5ef9e7b8da44c01ebc37f591d11c..85c4f0db09df5bfec39ed67b355799cb1709a6b9 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/core/UsersUtil.as +++ b/bigbluebutton-client/src/org/bigbluebutton/core/UsersUtil.as @@ -70,7 +70,7 @@ package org.bigbluebutton.core return false; } - public static function setUserEjected():void { + public static function setUserEjected():void { LiveMeeting.inst().me.ejectedFromMeeting = true; } @@ -302,19 +302,19 @@ package org.bigbluebutton.core public static function isAnyoneLocked():Boolean { return LiveMeeting.inst().users.isAnyUserLocked(); } - - + + public static function initLogData():Object { var logData:Object = new Object(); if (getInternalMeetingID() != null) { logData.user = UsersUtil.getUserData(); } logData.sessionToken = getUserSession(); - logData.connections = BBB.initConnectionManager().getConnectionIds(); - - var now:Date = new Date(); - logData.utcTime = now.getTime(); - logData.tzOffsetMin = now.getTimezoneOffset(); + logData.connections = BBB.initConnectionManager().getConnectionIds(); + + var now:Date = new Date(); + logData.utcTime = now.getTime(); + logData.tzOffsetMin = now.getTimezoneOffset(); return logData; } diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/managers/WebRTCDeskshareManager.as b/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/managers/WebRTCDeskshareManager.as index 510498d5f423b635dd20490fc2c0b5f6042a6b87..7f8aa3aa74ac72a525e9b93269f802fc6cb723f3 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/managers/WebRTCDeskshareManager.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/managers/WebRTCDeskshareManager.as @@ -18,8 +18,10 @@ */ package org.bigbluebutton.modules.screenshare.managers { - import com.asfusion.mate.events.Dispatcher; + import com.asfusion.mate.events.Dispatcher; + import flash.external.ExternalInterface; + import org.as3commons.logging.api.ILogger; import org.as3commons.logging.api.getClassLogger; import org.bigbluebutton.core.Options; @@ -73,7 +75,12 @@ package org.bigbluebutton.modules.screenshare.managers /*viewer being told there is no more stream*/ public function handleStreamStopEvent(args:Object):void { - LOGGER.debug("WebRTCDeskshareManager::handleStreamStopEvent"); + var logData:Object = UsersUtil.initLogData(); + logData.type = "webrtc"; + logData.tags = ["screenshare"]; + logData.logCode = "screenshare_stopped_stream"; + LOGGER.info(JSON.stringify(logData)); + sharing = false; viewWindowManager.handleViewWindowCloseEvent(); } @@ -85,7 +92,12 @@ package org.bigbluebutton.modules.screenshare.managers } private function stopWebRTCDeskshare():void { - LOGGER.debug("WebRTCDeskshareManager::stopWebRTCDeskshare"); + var logData:Object = UsersUtil.initLogData(); + logData.type = "webrtc"; + logData.tags = ["screenshare"]; + logData.logCode = "screenshare_stop_request"; + LOGGER.info(JSON.stringify(logData)); + viewWindowManager.stopViewing(); /* close the sharing window. The sharing window can also be open when going through @@ -113,7 +125,11 @@ package org.bigbluebutton.modules.screenshare.managers /*handle start sharing event*/ public function startSharing():void { - LOGGER.debug("WebRTCDeskshareManager::handleStartSharingEvent"); + var logData:Object = UsersUtil.initLogData(); + logData.type = "webrtc"; + logData.tags = ["screenshare"]; + logData.logCode = "screenshare_start_request"; + LOGGER.info(JSON.stringify(logData)); publishWindowManager.startSharing(); } @@ -135,19 +151,23 @@ package org.bigbluebutton.modules.screenshare.managers var isPresenter:Boolean = UsersUtil.amIPresenter(); LOGGER.debug("Received start viewing command when isPresenter==[{0}]",[isPresenter]); + var logData:Object = UsersUtil.initLogData(); + logData.type = "webrtc"; + logData.tags = ["screenshare"]; + logData.logCode = "screenshare_start_stream"; + LOGGER.info(JSON.stringify(logData)); + if(isPresenter && usingWebRTC) { publishWindowManager.startViewing(e.rtmp, e.videoWidth, e.videoHeight); globalDispatcher.dispatchEvent(new DeskshareToolbarEvent(DeskshareToolbarEvent.START)); } else { - if (!options.offerWebRTC || e == null || e.rtmp == null) { return; } viewWindowManager.startViewing(e.rtmp, e.videoWidth, e.videoHeight); } - - sharing = true; //TODO must uncomment this for the non-webrtc desktop share + sharing = true; //TODO must uncomment this for the non-webrtc desktop share } public function handleRequestStartSharingEvent(event:RequestToStartSharing):void { diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/managers/WebRTCPublishWindowManager.as b/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/managers/WebRTCPublishWindowManager.as index 11cbfc36aafce8be21d022da8dd302e5a60ddbbf..43f0a5fcb10f2c5695801a7fa4586c458d5d8be8 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/managers/WebRTCPublishWindowManager.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/managers/WebRTCPublishWindowManager.as @@ -19,9 +19,10 @@ package org.bigbluebutton.modules.screenshare.managers { - import com.asfusion.mate.events.Dispatcher; - import flash.events.TimerEvent; - import flash.utils.Timer; + import com.asfusion.mate.events.Dispatcher; + + import flash.utils.Timer; + import org.as3commons.logging.api.ILogger; import org.as3commons.logging.api.getClassLogger; import org.bigbluebutton.common.IBbbModuleWindow; diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/view/components/ScreensharePublishWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/view/components/ScreensharePublishWindow.mxml index 6439b92710f9ae8444d8970014078f2613609d6a..86c39d8bc93607930611a5e866d30af87f573fd4 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/view/components/ScreensharePublishWindow.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/view/components/ScreensharePublishWindow.mxml @@ -192,22 +192,22 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } private function handleStartShareRequestSuccessEvent(event:StartShareRequestSuccessEvent):void { - var logData:Object = UsersUtil.initLogData(); - logData.session = ScreenshareModel.getInstance().session; - logData.streamId = ScreenshareModel.getInstance().streamId; - logData.tags = ["screenshare"]; - logData.logCode = "screenshare_request_success"; - LOGGER.warn(JSON.stringify(logData)); + var logData:Object = UsersUtil.initLogData(); + logData.session = ScreenshareModel.getInstance().session; + logData.streamId = ScreenshareModel.getInstance().streamId; + logData.tags = ["screenshare"]; + logData.logCode = "screenshare_request_success"; + LOGGER.warn(JSON.stringify(logData)); } private function handleScreenShareShareStoppedEvent(event:ShareStoppedEvent):void { - var logData:Object = UsersUtil.initLogData(); - logData.reason = event.reason; - logData.session = ScreenshareModel.getInstance().session; - logData.streamId = ScreenshareModel.getInstance().streamId; - logData.tags = ["screenshare"]; - logData.logCode = "screenshare_end_reason"; - LOGGER.warn(JSON.stringify(logData)); + var logData:Object = UsersUtil.initLogData(); + logData.reason = event.reason; + logData.session = ScreenshareModel.getInstance().session; + logData.streamId = ScreenshareModel.getInstance().streamId; + logData.tags = ["screenshare"]; + logData.logCode = "screenshare_end_reason"; + LOGGER.warn(JSON.stringify(logData)); var showReason:Boolean = false; @@ -232,38 +232,41 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. cancelBtn.visible = cancelBtn.includeInLayout = true; startBtn.visible = startBtn.includeInLayout = false; stopBtn.visible = stopBtn.includeInLayout = false; - - } else { closeWindow(); } } - private function handleScreenSharePausedEvent(event:ScreenSharePausedEvent):void { - if (videoWrapper != null && video != null && video.parent == videoWrapper) { - videoWrapper.removeChild(video); - - if (paused) { - var logData:Object = UsersUtil.initLogData(); - logData.tags = ["screenshare"]; - logData.session = ScreenshareModel.getInstance().session; - logData.streamId = ScreenshareModel.getInstance().streamId; - logData.logCode = "screenshare_paused"; - LOGGER.warn(JSON.stringify(logData)); - - pauseBtn.enabled = true; - pauseBtn.visible = pauseBtn.includeInLayout = false; - restartBtn.visible = restartBtn.includeInLayout = true; - pauseBox.visible = pauseBox.includeInLayout = true; - videoWrapper.visible = videoWrapper.includeInLayout = false; - } else { - switchView(true); + private function handleScreenSharePausedEvent(event:ScreenSharePausedEvent):void { + if (videoWrapper != null && video != null && video.parent == videoWrapper) { + videoWrapper.removeChild(video); + + if (paused) { + var logData:Object = UsersUtil.initLogData(); + logData.tags = ["screenshare"]; + logData.session = ScreenshareModel.getInstance().session; + logData.streamId = ScreenshareModel.getInstance().streamId; + logData.logCode = "screenshare_paused"; + LOGGER.warn(JSON.stringify(logData)); + + pauseBtn.enabled = true; + pauseBtn.visible = pauseBtn.includeInLayout = false; + restartBtn.visible = restartBtn.includeInLayout = true; + pauseBox.visible = pauseBox.includeInLayout = true; + videoWrapper.visible = videoWrapper.includeInLayout = false; + } else { + switchView(true); + } } - } - } + } public function shareScreen(fullScreen:Boolean):void { - + var logData:Object = UsersUtil.initLogData(); + logData.type = "java"; + logData.tags = ["screenshare"]; + logData.logCode = "screenshare_start_request"; + LOGGER.info(JSON.stringify(logData)); + startBtn.enabled = false; var shareStartEvent:ShareStartEvent = new ShareStartEvent(); dispatchEvent(shareStartEvent); @@ -277,18 +280,24 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. var authToken:String = ScreenshareModel.getInstance().authToken; var jnlp: String = ScreenshareModel.getInstance().jnlp; - var logData:Object = UsersUtil.initLogData(); - logData.tags = ["screenshare"]; - logData.session = ScreenshareModel.getInstance().session; - logData.streamId = ScreenshareModel.getInstance().streamId; - logData.logCode = "screenshare_start_sharing"; - logData.token = authToken; - LOGGER.warn(JSON.stringify(logData)); + var logData:Object = UsersUtil.initLogData(); + logData.tags = ["screenshare"]; + logData.session = ScreenshareModel.getInstance().session; + logData.streamId = ScreenshareModel.getInstance().streamId; + logData.logCode = "screenshare_start_sharing"; + logData.token = authToken; + LOGGER.warn(JSON.stringify(logData)); ExternalInterface.call("startScreensharing", jnlp, UsersUtil.getInternalMeetingID(), authToken, fullScreen); } - public function stopSharing():void {; + public function stopSharing():void { + var logData:Object = UsersUtil.initLogData(); + logData.type = "java"; + logData.tags = ["screenshare"]; + logData.logCode = "screenshare_stop_request"; + LOGGER.info(JSON.stringify(logData)); + if (streaming) { stopStream(); } @@ -298,11 +307,15 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. sharingFullScreen = false; streaming = false; - - //closeWindow(); } public function pauseSharing():void { + var logData:Object = UsersUtil.initLogData(); + logData.type = "java"; + logData.tags = ["screenshare"]; + logData.logCode = "screenshare_pause_request"; + LOGGER.info(JSON.stringify(logData)); + if (!paused) { paused = true; if (streaming) { @@ -335,7 +348,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } private function handleStartViewStreamEvent(event:ViewStreamEvent):void{ - var width: int = ScreenshareModel.getInstance().width; var height: int = ScreenshareModel.getInstance().height; var streamId: String = ScreenshareModel.getInstance().streamId; @@ -343,6 +355,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } private function startPreviewStream(nc:NetConnection, streamId:String, capWidth:Number, capHeight:Number):void{ + var logData:Object = UsersUtil.initLogData(); + logData.type = "java"; + logData.tags = ["screenshare"]; + logData.logCode = "screenshare_start_preview"; + LOGGER.info(JSON.stringify(logData)); switchView(false); @@ -393,10 +410,21 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } public function onMetaData(info:Object):void{ - LOGGER.debug("metadata: width=" + info.width + " height=" + info.height); + var logData:Object = UsersUtil.initLogData(); + logData.type = "java"; + logData.tags = ["screenshare"]; + logData.logCode = "screenshare_metadata"; + logData.metadata = info; + LOGGER.debug(JSON.stringify(logData)); } private function stopStream():void{ + var logData:Object = UsersUtil.initLogData(); + logData.type = "java"; + logData.tags = ["screenshare"]; + logData.logCode = "screenshare_stop_stream"; + LOGGER.info(JSON.stringify(logData)); + streaming = false; captureHeight = Capabilities.screenResolutionY; captureWidth = Capabilities.screenResolutionX; @@ -404,7 +432,12 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } private function onAsyncError(e:AsyncErrorEvent):void{ - LOGGER.debug("VIdeoWindow::asyncerror " + e.toString()); + var logData:Object = UsersUtil.initLogData(); + logData.type = "java"; + logData.tags = ["screenshare"]; + logData.logCode = "screenshare_async_error"; + logData.error = e.toString(); + LOGGER.warn(JSON.stringify(logData)); } private function onNetStatus(e:NetStatusEvent):void{ @@ -431,7 +464,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. dispatchEvent(new ShareWindowEvent(ShareWindowEvent.CLOSE)); if (!soundPlayed) { - trace("playing from java publish window"); + LOGGER.debug("playing from java publish window"); var tSC:SoundChannel = noticeSound.play(0, 0, new SoundTransform(0.25)); soundPlayed = true; } @@ -441,7 +474,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. shareScreen(sharingFullScreen); } - /* * Override the close handler. We want the Event Map to send a message to * the MDIManager to close this window; @@ -473,12 +505,12 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. shareTypeProvider = [ResourceUtil.getInstance().getString('bbb.screensharePublish.shareType.fullScreen'), ResourceUtil.getInstance().getString('bbb.screensharePublish.shareType.region')]; - var options:ScreenshareOptions = Options.getOptions(ScreenshareOptions) as ScreenshareOptions; - - if (options.shareRegionDefault) { - shareTypeProvider = [ResourceUtil.getInstance().getString('bbb.screensharePublish.shareType.region'), - ResourceUtil.getInstance().getString('bbb.screensharePublish.shareType.fullScreen')]; - } + var options:ScreenshareOptions = Options.getOptions(ScreenshareOptions) as ScreenshareOptions; + + if (options.shareRegionDefault) { + shareTypeProvider = [ResourceUtil.getInstance().getString('bbb.screensharePublish.shareType.region'), + ResourceUtil.getInstance().getString('bbb.screensharePublish.shareType.fullScreen')]; + } setHelpText(); } @@ -556,14 +588,14 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } private function onStartButtonClick():void { - var options:ScreenshareOptions = Options.getOptions(ScreenshareOptions) as ScreenshareOptions; - if (options.shareRegionDefault) { - // Region is first option in dropdown. - shareScreen(shareTypeCombo.selectedIndex != 0); - } else { - // Fullscreen is first option in dropdown. - shareScreen(shareTypeCombo.selectedIndex == 0); - } + var options:ScreenshareOptions = Options.getOptions(ScreenshareOptions) as ScreenshareOptions; + if (options.shareRegionDefault) { + // Region is first option in dropdown. + shareScreen(shareTypeCombo.selectedIndex != 0); + } else { + // Fullscreen is first option in dropdown. + shareScreen(shareTypeCombo.selectedIndex == 0); + } } private function switchView(showHelp:Boolean):void { diff --git a/bigbluebutton-html5/client/compatibility/kurento-extension.js b/bigbluebutton-html5/client/compatibility/kurento-extension.js index 2229844ae4d938cd16b3cd7ff3014555798cca90..060a677a6ef39bc2fc62a44152cc503c6b345aab 100644 --- a/bigbluebutton-html5/client/compatibility/kurento-extension.js +++ b/bigbluebutton-html5/client/compatibility/kurento-extension.js @@ -476,12 +476,8 @@ Kurento.prototype.setAudio = function (tag) { Kurento.prototype.listenOnly = function () { var self = this; - const remoteVideo = document.getElementById(this.renderTag); - remoteVideo.muted = true; if (!this.webRtcPeer) { var options = { - audioStream: this.inputStream, - remoteVideo, onicecandidate : this.onListenOnlyIceCandidate.bind(this), mediaConstraints: { audio: true, diff --git a/bigbluebutton-html5/client/compatibility/kurento-utils.js b/bigbluebutton-html5/client/compatibility/kurento-utils.js index e3b8a003d39e81798390c59f75bbf22d5a46d8a0..5057232faa7ff13fe60123f90241ebd574e25986 100644 --- a/bigbluebutton-html5/client/compatibility/kurento-utils.js +++ b/bigbluebutton-html5/client/compatibility/kurento-utils.js @@ -38,11 +38,13 @@ function noop(error) { logger.error(error); } function trackStop(track) { - track.stop && track.stop(); + track && track.stop && track.stop(); } function streamStop(stream) { - stream.getTracks().forEach(trackStop); + let track = stream.track; + trackStop(track); } + var dumpSDP = function (description) { if (typeof description === 'undefined' || description === null) { return ''; @@ -67,7 +69,9 @@ function bufferizeCandidates(pc, onerror) { break; case 'stable': if (pc.remoteDescription) { - pc.addIceCandidate(candidate, callback, callback); + pc.addIceCandidate(candidate).then(callback).catch(err => { + callback(err); + }); break; } default: @@ -228,7 +232,7 @@ function WebRtcPeer(mode, options, callback) { candidategatheringdone = true; } }); - pc.ontrack = options.onaddstream; + pc.onaddtrack = options.onaddstream; pc.onnegotiationneeded = options.onnegotiationneeded; this.on('newListener', function (event, listener) { if (event === 'icecandidate' || event === 'candidategatheringdone') { @@ -300,7 +304,8 @@ function WebRtcPeer(mode, options, callback) { }).then(() => { remoteVideo.muted = false; played = true; attempt = 0;}); } } - var stream = pc.getRemoteStreams()[0]; + + let stream = self.getRemoteStream(); remoteVideo.oncanplaythrough = function() { playVideo(); @@ -338,10 +343,10 @@ function WebRtcPeer(mode, options, callback) { if (pc.signalingState === 'closed') { return callback('PeerConnection is closed'); } - pc.setRemoteDescription(answer, function () { + pc.setRemoteDescription(answer).then(function () { setRemoteVideo(); callback(); - }, callback); + }).catch(callback); }; this.processOffer = function (sdpOffer, callback) { callback = callback.bind(this); @@ -398,10 +403,10 @@ function WebRtcPeer(mode, options, callback) { self.showLocalVideo(); } if (videoStream) { - pc.addStream(videoStream); + videoStream.getTracks().forEach(track => pc.addTrack(track, videoStream)); } if (audioStream) { - pc.addStream(audioStream); + audioStream.getTracks().forEach(track => pc.addTrack(track, audioStream)); } var browser = parser.getBrowser(); if (mode === 'sendonly' && (browser.name === 'Chrome' || browser.name === 'Chromium') && browser.major === 39) { @@ -459,23 +464,28 @@ function createEnableDescriptor(type) { get: function () { if (!this.peerConnection) return; - var streams = this.peerConnection.getLocalStreams(); - if (!streams.length) + + var senders = this.peerConnection.getSenders(); + if (!senders.length) return; - for (var i = 0, stream; stream = streams[i]; i++) { - var tracks = stream[method](); - for (var j = 0, track; track = tracks[j]; j++) - if (!track.enabled) - return false; - } + + senders.forEach(sender => { + let track = sender.track; + if (!track.enabled && track.kind === type) { + return false; + } + }); return true; }, set: function (value) { function trackSetEnable(track) { track.enabled = value; } - this.peerConnection.getLocalStreams().forEach(function (stream) { - stream[method]().forEach(trackSetEnable); + this.peerConnection.getSenders().forEach(function (stream) { + let track = stream.track; + if (track.kind === type) { + trackSetEnable(track); + } }); } }; @@ -493,15 +503,37 @@ Object.defineProperties(WebRtcPeer.prototype, { 'audioEnabled': createEnableDescriptor('Audio'), 'videoEnabled': createEnableDescriptor('Video') }); -WebRtcPeer.prototype.getLocalStream = function (index) { - if (this.peerConnection) { - return this.peerConnection.getLocalStreams()[index || 0]; - } +WebRtcPeer.prototype.getLocalStream = function () { + if (this.localStream) { + return this.localStream; + } + + if (this.peerConnection) { + this.localStream = new MediaStream(); + this.peerConnection.getSenders().forEach((ls) => { + let track = ls.track; + if (track && !track.muted) { + this.localStream.addTrack(track); + } + }); + return this.localStream; + } }; -WebRtcPeer.prototype.getRemoteStream = function (index) { - if (this.peerConnection) { - return this.peerConnection.getRemoteStreams()[index || 0]; - } +WebRtcPeer.prototype.getRemoteStream = function () { + if (this.remoteStream) { + return this.remoteStream; + } + + if (this.peerConnection) { + this.remoteStream = new MediaStream(); + this.peerConnection.getReceivers().forEach((rs) => { + let track = rs.track; + if (track && !track.muted) { + this.remoteStream.addTrack(track); + } + }); + return this.remoteStream; + } }; WebRtcPeer.prototype.dispose = function () { // logger.debug('Disposing WebRtcPeer'); @@ -516,7 +548,13 @@ WebRtcPeer.prototype.dispose = function () { if (pc) { if (pc.signalingState === 'closed') return; - pc.getLocalStreams().forEach(streamStop); + pc.getSenders().forEach(streamStop); + if (this.remoteStream) { + this.remoteStream = null; + } + if (this.localStream) { + this.localStream = null; + } pc.close(); } } catch (err) { diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js b/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js index 868cd319a5afbc0261f5bb23d1647cbb56eeff71..2608e1dd42b072e305a966c5337a7458da0990a1 100644 --- a/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js +++ b/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js @@ -62,10 +62,6 @@ export default class KurentoAudioBridge extends BaseAudioBridge { this.voiceBridge = voiceBridge; } - exitAudio(listenOnly) { - window.kurentoExitAudio(); - } - joinAudio({ isListenOnly, inputStream }, callback) { return new Promise(async (resolve, reject) => { this.callback = callback; @@ -86,7 +82,18 @@ export default class KurentoAudioBridge extends BaseAudioBridge { inputStream, }; - const onSuccess = ack => resolve(this.callback({ status: this.baseCallStates.started })); + const onSuccess = ack => { + const { webRtcPeer } = window.kurentoManager.kurentoAudio; + if (webRtcPeer) { + const audioTag = document.getElementById(MEDIA_TAG); + const stream = webRtcPeer.getRemoteStream(); + audioTag.pause(); + audioTag.srcObject = stream; + audioTag.muted = false; + audioTag.play(); + } + resolve(this.callback({ status: this.baseCallStates.started })); + }; const onFail = error => { const { reason } = error; diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/changeRole.js b/bigbluebutton-html5/imports/api/users/server/modifiers/changeRole.js index 42842aa520be978ef7b941f5a0290d473db74232..b948515b6ad886e141a441fd13fb6b4fe5abaa31 100644 --- a/bigbluebutton-html5/imports/api/users/server/modifiers/changeRole.js +++ b/bigbluebutton-html5/imports/api/users/server/modifiers/changeRole.js @@ -10,8 +10,7 @@ export default function changeRole(role, status, userId, meetingId, changedBy) { userId, }; - const action = status ? '$push' : '$pop'; - + const action = status ? '$addToSet' : '$pull'; const user = Users.findOne(selector); const modifier = { diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx index 63cd05048d733abbcfae9951feddd8af8ad7e626..53da41f6e3e1d6f07541a34931f204c97a0df078 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx @@ -407,11 +407,12 @@ class VideoProvider extends Component { options.configuration.iceServers = iceServers; } - let WebRtcPeerObj = window.kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly; - + let WebRtcPeerObj; if (shareWebcam) { WebRtcPeerObj = window.kurentoUtils.WebRtcPeer.WebRtcPeerSendonly; this.shareWebcam(); + } else { + WebRtcPeerObj = window.kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly; } this.webRtcPeers[id] = new WebRtcPeerObj(options, (error) => { diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js index 1d882823322bf83f60dfe02d48ed241cf944718b..9ca8ebb0e5c2041a48c1534df3931b4ee626e50d 100755 --- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js +++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js @@ -7,6 +7,7 @@ import VoiceUsers from '/imports/api/voice-users'; import SIPBridge from '/imports/api/audio/client/bridge/sip'; import logger from '/imports/startup/client/logger'; import { notify } from '/imports/ui/services/notification'; +import browser from 'browser-detect'; const MEDIA = Meteor.settings.public.media; const MEDIA_TAG = MEDIA.mediaTag; @@ -130,9 +131,10 @@ class AudioManager { .then(() => this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this))); } - joinListenOnly(retries = 0) { + async joinListenOnly(retries = 0) { this.isListenOnly = true; this.isEchoTest = false; + const { name } = browser(); // The kurento bridge isn't a full audio bridge yet, so we have to differ it const bridge = this.useKurento? this.listenOnlyBridge : this.bridge; @@ -142,18 +144,24 @@ class AudioManager { inputStream: this.createListenOnlyStream(), }; + // Webkit ICE restrictions demand a capture device permission to release + // host candidates + if (name == 'safari') { + await this.askDevicesPermissions(); + } + // We need this until we upgrade to SIP 9x. See #4690 const iceGatheringErr = 'ICE_TIMEOUT'; const iceGatheringTimeout = new Promise((resolve, reject) => { setTimeout(reject, 12000, iceGatheringErr); }); - const handleIceGatheringError = (err) => { + const handleListenOnlyError = async (err) => { if (iceGatheringTimeout) { clearTimeout(iceGatheringTimeout); } - logger.error('Listen only error:', err); + logger.error('Listen only error:', err, 'on try', retries); const error = { type: 'MEDIA_ERROR', message: this.messages.error.MEDIA_ERROR, @@ -167,19 +175,23 @@ class AudioManager { iceGatheringTimeout, ])) .catch(async (err) => { - // If theres a iceGathering timeout we retry to join until MAX_LISTEN_ONLY_RETRIES - if (err === iceGatheringErr && retries < MAX_LISTEN_ONLY_RETRIES) { + if (retries < MAX_LISTEN_ONLY_RETRIES) { // Fallback to SIP.js listen only in case of failure if (this.useKurento) { + // Exit previous SFU session and clean audio tag state + window.kurentoExitAudio(); this.useKurento = false; + let audio = document.querySelector(MEDIA_TAG); + audio.muted = false; } + try { await this.joinListenOnly(++retries); } catch (err) { - return handleIceGatheringError(err); + return handleListenOnlyError(err); } } else { - handleIceGatheringError(err); + handleListenOnlyError(err); } }); } @@ -239,9 +251,6 @@ class AudioManager { if (!this.isEchoTest) { this.notify(this.messages.info.JOINED_AUDIO); } - - // Restore the default listen only bridge - this.useKurento = Meteor.settings.public.kurento.enableListenOnly; } onTransferStart() { @@ -264,9 +273,6 @@ class AudioManager { if (!this.error && !this.isEchoTest) { this.notify(this.messages.info.LEFT_AUDIO); } - - // Restore the default listen only bridge - this.useKurento = Meteor.settings.public.kurento.enableListenOnly; } callStateCallback(response) { diff --git a/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml b/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml index 1a053f0b7def608749d8b9a2c52c9da87be011a6..d2adcf0cc586fffbebf877f00a44f49c8203373f 100755 --- a/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml +++ b/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml @@ -73,7 +73,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <bean id="textFileCreator" class="org.bigbluebutton.presentation.imp.TextFileCreatorImp"/> <bean id="svgImageCreator" class="org.bigbluebutton.presentation.imp.SvgImageCreatorImp"> - <property name="imageMagickDir" value="${imageMagickDir}"/> + <property name="imageTagThreshold" value="${imageTagThreshold}"/> + <property name="pathsThreshold" value="${placementsThreshold}"/> </bean> <bean id="generatedSlidesInfoHelper" class="org.bigbluebutton.presentation.GeneratedSlidesInfoHelperImp"/> diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy index 59dadbe41c25e64abc0378480a61eeb513168ba3..981ec4842404620ecb52460dd4438c9b9440df01 100755 --- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy +++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy @@ -1838,8 +1838,7 @@ class ApiController { requestBody = StringUtils.isEmpty(requestBody) ? null : requestBody; if (requestBody == null) { - downloadAndProcessDocument(presentationService.defaultUploadedPresentation, conf.getInternalId(), - true /* default presentation */ ); + downloadAndProcessDocument(presentationService.defaultUploadedPresentation, conf.getInternalId(), true /* default presentation */, ''); } else { log.debug "Request body: \n" + requestBody; def xml = new XmlSlurper().parseText(requestBody); @@ -1850,7 +1849,12 @@ class ApiController { // need to iterate over presentation files and process them module.children().each { document -> if (!StringUtils.isEmpty(document.@url.toString())) { - downloadAndProcessDocument(document.@url.toString(), conf.getInternalId(), true /* default presentation */); + def fileName; + if (!StringUtils.isEmpty(document.@filename.toString())) { + log.debug("user provided filename: [${module.@filename}]"); + fileName = document.@filename.toString(); + } + downloadAndProcessDocument(document.@url.toString(), conf.getInternalId(), true /* default presentation */, fileName); } else if (!StringUtils.isEmpty(document.@name.toString())) { def b64 = new Base64() def decodedBytes = b64.decode(document.text().getBytes()) @@ -1884,9 +1888,15 @@ class ApiController { } - def downloadAndProcessDocument(address, meetingId, current) { - log.debug("ApiController#downloadAndProcessDocument(${address}, ${meetingId})"); - String presFilename = address.tokenize("/")[-1]; + def downloadAndProcessDocument(address, meetingId, current, fileName) { + log.debug("ApiController#downloadAndProcessDocument(${address}, ${meetingId}, ${fileName})"); + String presFilename; + if (StringUtils.isEmpty(fileName)) { + presFilename = address.tokenize("/")[-1]; + } else { + presFilename = fileName; + } + def filenameExt = FilenameUtils.getExtension(presFilename); String presentationDir = presentationService.getPresentationDir() @@ -1900,7 +1910,7 @@ class ApiController { def pres = new File(newFilePath) processUploadedFile(meetingId, presId, presFilename, pres, current); } else { - log.error("Failed to download presentation=[${address}], meeting=[${meetingId}]") + log.error("Failed to download presentation=[${address}], meeting=[${meetingId}], fileName=[${fileName}]") } } } diff --git a/labs/bbb-webrtc-sfu/lib/audio/AudioManager.js b/labs/bbb-webrtc-sfu/lib/audio/AudioManager.js index 8997af06d0b77288cdf28bcbcc369eb8a8b90680..54d8eebd3d32633dfc053af49b31fc96ed51e8e4 100755 --- a/labs/bbb-webrtc-sfu/lib/audio/AudioManager.js +++ b/labs/bbb-webrtc-sfu/lib/audio/AudioManager.js @@ -21,6 +21,7 @@ module.exports = class AudioManager extends BaseManager { this._meetings = {}; this._trackMeetingTermination(); this.messageFactory(this._onMessage); + this._iceQueues = {}; } _trackMeetingTermination () { @@ -50,15 +51,12 @@ module.exports = class AudioManager extends BaseManager { async _onMessage(message) { Logger.debug(this._logPrefix, 'Received message [' + message.id + '] from connection', message.connectionId); - let session; - let sessionId = message.voiceBridge; let voiceBridge = sessionId; let connectionId = message.connectionId; - if(this._sessions[sessionId]) { - session = this._sessions[sessionId]; - } + let session = this._fetchSession(sessionId); + let iceQueue = this._fetchIceQueue(sessionId+connectionId); switch (message.id) { case 'start': @@ -76,6 +74,9 @@ module.exports = class AudioManager extends BaseManager { const sdpAnswer = await session.start(sessionId, connectionId, sdpOffer, caleeName, userId, userName); Logger.info(this._logPrefix, "Started presenter ", sessionId, " for connection", connectionId); + // Empty the ICE queue + this._flushIceQueue(session, iceQueue); + session.once(C.MEDIA_SERVER_OFFLINE, async (event) => { const errorMessage = this._handleError(this._logPrefix, connectionId, caleeName, C.RECV_ROLE, errors.MEDIA_SERVER_OFFLINE); errorMessage.id = 'webRTCAudioError'; @@ -117,12 +118,13 @@ module.exports = class AudioManager extends BaseManager { session.onIceCandidate(message.candidate, connectionId); } else { Logger.warn(this._logPrefix, "There was no audio session for onIceCandidate for", sessionId, ". There should be a queue here"); + iceQueue.push(message.candidate); } break; case 'close': Logger.info(this._logPrefix, 'Connection ' + connectionId + ' closed'); - + this._deleteIceQueue(sessionId+connectionId); if (typeof session !== 'undefined') { Logger.info(this._logPrefix, "Stopping viewer " + sessionId); session.stopListener(message.connectionId); diff --git a/labs/bbb-webrtc-sfu/lib/base/BaseManager.js b/labs/bbb-webrtc-sfu/lib/base/BaseManager.js index 8a88e671fc1cc671d87eaa75a8399a0f8d405ecc..ee18a1e0e9043c586fb7a64d90c6b8ac39f73a7f 100644 --- a/labs/bbb-webrtc-sfu/lib/base/BaseManager.js +++ b/labs/bbb-webrtc-sfu/lib/base/BaseManager.js @@ -64,6 +64,12 @@ module.exports = class BaseManager { } } + _deleteIceQueue (sessionId) { + if (this._iceQueues[sessionId]) { + delete this._iceQueues[sessionId]; + } + } + _killConnectionSessions (connectionId) { const keys = Object.keys(this._sessions); keys.forEach((sessionId) => { diff --git a/record-and-playback/presentation/playback/presentation/2.0/playback.css b/record-and-playback/presentation/playback/presentation/2.0/playback.css index 3ff94d4664c0c04abc2dcd48f2bccab6da73626c..98ed7aa4131c1b619451177281c05fb7481c5a57 100644 --- a/record-and-playback/presentation/playback/presentation/2.0/playback.css +++ b/record-and-playback/presentation/playback/presentation/2.0/playback.css @@ -54,6 +54,10 @@ html, .acorn-controls { min-width: 310px; } +p { + font-size: inherit; +} + /* Swappable components have different settings depending on where they are */ #main-section #presentation-area { diff --git a/record-and-playback/presentation/scripts/publish/presentation.rb b/record-and-playback/presentation/scripts/publish/presentation.rb index d3460166d5d6c0e6b21c524dd571829ac44c6412..e793e02cb269dcc54e31867d28ed2dbdd17e0458 100755 --- a/record-and-playback/presentation/scripts/publish/presentation.rb +++ b/record-and-playback/presentation/scripts/publish/presentation.rb @@ -368,7 +368,7 @@ def svg_render_shape_poll(g, slide, shape) result = JSON.load(shape[:result]) num_responders = shape[:num_responders] - presentation = shape[:presentation] + presentation = slide[:presentation] max_num_votes = result.map{ |r| r['num_votes'] }.max dat_file = "#{$process_dir}/poll_result#{poll_id}.dat"