diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 54ae869e..9a0040d6 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = False -current_version = v4.10.0 +current_version = v4.11.0 parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? serialize = {major}.{minor}.{patch}-{release}{build} diff --git a/DEVELOPING.md b/DEVELOPING.md index ba1a53b3..c604e9e7 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -81,8 +81,7 @@ During development the version number should end in "-alpha.x" or "-beta.x", whe ### Branches -* `dev` - the main development branch. -* `master` - reflects the latest stable release. +* `main` - the main development branch. * `feat.foo` - feature branches. these are used for longer running tasks that cannot be accomplished in one commit. once merged into master, these branches should be deleted. * `vx.x.x` - if development for a future version/milestone has begun while master is working towards a sooner diff --git a/README.md b/README.md index 0bdbda12..fcdde081 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ When you use Maven as your build tool, you can manage dependencies in the `pom.x com.tokbox opentok-server-sdk - 4.10.0 + 4.11.0 ``` @@ -44,7 +44,7 @@ When you use Gradle as your build tool, you can manage dependencies in the `buil ```groovy dependencies { - compile group: 'com.tokbox', name: 'opentok-server-sdk', version: '4.10.0' + compile group: 'com.tokbox', name: 'opentok-server-sdk', version: '4.11.0' } ``` diff --git a/build.gradle b/build.gradle index c83960e5..0277e57c 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ plugins { group = 'com.tokbox' archivesBaseName = 'opentok-server-sdk' -version = '4.10.0' +version = '4.11.0' sourceCompatibility = "1.8" targetCompatibility = "1.8" @@ -27,10 +27,10 @@ dependencies { implementation 'commons-lang:commons-lang:2.6' implementation 'commons-validator:commons-validator:1.7' implementation 'org.asynchttpclient:async-http-client:2.12.3' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.0' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2' implementation 'commons-codec:commons-codec:1.15' implementation 'org.bitbucket.b_c:jose4j:0.9.3' - implementation 'io.netty:netty-handler:4.1.89.Final' + implementation 'io.netty:netty-handler:4.1.93.Final' } task sourcesJar(type: Jar) { diff --git a/src/main/java/com/opentok/Broadcast.java b/src/main/java/com/opentok/Broadcast.java index 515f0ced..f0d3d134 100644 --- a/src/main/java/com/opentok/Broadcast.java +++ b/src/main/java/com/opentok/Broadcast.java @@ -34,14 +34,17 @@ public enum StreamMode { */ AUTO, /** - * Strams will be included in the archive based on calls to the + * Streams will be included in the archive based on calls to the * {@link OpenTok#addBroadcastStream(String, String, boolean, boolean)} and * {@link OpenTok#removeBroadcastStream(String, String)} methods. */ MANUAL; @JsonValue - public String toString() { return super.toString().toLowerCase(); } + @Override + public String toString() { + return name().toLowerCase(); + } } @JsonProperty private String id; @@ -52,8 +55,10 @@ public enum StreamMode { @JsonProperty private String resolution; @JsonProperty private String status; @JsonProperty private String multiBroadcastTag; + @JsonProperty private boolean hasAudio = true; + @JsonProperty private boolean hasVideo = true; @JsonProperty private StreamMode streamMode = StreamMode.AUTO; - private List rtmpList = new ArrayList<>(); + private List rtmpList = new ArrayList<>(5); private String hls; /** @@ -165,13 +170,18 @@ public List getRtmpList() { return rtmpList; } - @Override - public String toString() { - try { - return new ObjectMapper().writeValueAsString(this); - } catch (JsonProcessingException e) { - return ""; - } + /** + * Whether the broadcast has audio (true) or not (false). + */ + public boolean hasAudio() { + return hasAudio; + } + + /** + * Whether the broadcast has video (true) or not (false). + */ + public boolean hasVideo() { + return hasVideo; } /** @@ -182,4 +192,12 @@ public StreamMode getStreamMode() { return streamMode; } + @Override + public String toString() { + try { + return new ObjectMapper().writeValueAsString(this); + } catch (JsonProcessingException e) { + return ""; + } + } } diff --git a/src/main/java/com/opentok/BroadcastProperties.java b/src/main/java/com/opentok/BroadcastProperties.java index 721b3c3f..e55c2db3 100644 --- a/src/main/java/com/opentok/BroadcastProperties.java +++ b/src/main/java/com/opentok/BroadcastProperties.java @@ -19,19 +19,23 @@ * {@link OpenTok#startBroadcast(String sessionId, BroadcastProperties properties)} method. */ public class BroadcastProperties { - private BroadcastLayout layout; - private int maxDuration; - private boolean hasHls; - private List rtmpList; - private String resolution; - private String multiBroadcastTag; - private StreamMode streamMode; - private Hls hls; + private final BroadcastLayout layout; + private final int maxDuration; + private final boolean hasHls; + private final boolean hasAudio; + private final boolean hasVideo; + private final List rtmpList; + private final String resolution; + private final String multiBroadcastTag; + private final StreamMode streamMode; + private final Hls hls; private BroadcastProperties(Builder builder) { this.layout = builder.layout; this.maxDuration = builder.maxDuration; this.hasHls = builder.hasHls; + this.hasAudio = builder.hasAudio; + this.hasVideo = builder.hasVideo; this.hls = builder.hls; this.rtmpList = builder.rtmpList; this.resolution = builder.resolution; @@ -48,9 +52,11 @@ public static class Builder { private BroadcastLayout layout = new BroadcastLayout(BroadcastLayout.Type.BESTFIT); private int maxDuration = 7200; private boolean hasHls = false; + private boolean hasAudio = true; + private boolean hasVideo = true; private String multiBroadcastTag; private Hls hls; - private List rtmpList = new ArrayList<>(); + private final List rtmpList = new ArrayList<>(5); private String resolution = "640x480"; private StreamMode streamMode = StreamMode.AUTO; @@ -107,6 +113,30 @@ public Builder hls(Hls hls) { return hasHls((this.hls = hls) != null); } + /** + * Whether to include audio in the broadcast ({@code true} by default). + * + * @param hasAudio {@code true} if audio should be included, {@code false} otherwise. + * + * @return The BroadcastProperties.Builder object with the hasAudio setting. + */ + public Builder hasAudio(boolean hasAudio) { + this.hasAudio = hasAudio; + return this; + } + + /** + * Whether to include video in the broadcast ({@code true} by default). + * + * @param hasVideo {@code true} if video should be included, {@code false} otherwise. + * + * @return The BroadcastProperties.Builder object with the hasVideo setting. + */ + public Builder hasVideo(boolean hasVideo) { + this.hasVideo = hasVideo; + return this; + } + /** * Call this method to set a list of RTMP broadcast streams. There is a limit of * 5 RTMP streams. @@ -115,7 +145,7 @@ public Builder hls(Hls hls) { * * @return The BroadcastProperties.Builder object with the list of RtmpProperties setting. */ - public Builder addRtmpProperties (RtmpProperties rtmpProps) throws InvalidArgumentException { + public Builder addRtmpProperties(RtmpProperties rtmpProps) throws InvalidArgumentException { if (this.rtmpList.size() >= 5) { throw new InvalidArgumentException("Cannot add more than 5 RtmpProperties properties"); } @@ -206,6 +236,20 @@ public boolean hasHls() { return hasHls; } + /** + * Whether the broadcast has audio (true) or not (false). + */ + public boolean hasAudio() { + return hasAudio; + } + + /** + * Whether the broadcast has video (true) or not (false). + */ + public boolean hasVideo() { + return hasVideo; + } + /** * The HLS configuration object, or null if {@link BroadcastProperties#hasHls} is false. */ @@ -219,6 +263,7 @@ public Hls hls() { public List rtmpList() { return rtmpList; } + /** * Returns the resolution of the broadcast. */ @@ -236,5 +281,7 @@ public String getMultiBroadcastTag() { /** * The stream mode of the broadcast. */ - public StreamMode streamMode() { return streamMode; } + public StreamMode streamMode() { + return streamMode; + } } diff --git a/src/main/java/com/opentok/Hls.java b/src/main/java/com/opentok/Hls.java index 9ff38138..400f4ac9 100644 --- a/src/main/java/com/opentok/Hls.java +++ b/src/main/java/com/opentok/Hls.java @@ -13,7 +13,6 @@ * {@link BroadcastProperties#hls()} method. */ public class Hls { - private final boolean dvr; private final boolean lowLatency; diff --git a/src/main/java/com/opentok/Resolution.java b/src/main/java/com/opentok/Resolution.java new file mode 100644 index 00000000..afd43803 --- /dev/null +++ b/src/main/java/com/opentok/Resolution.java @@ -0,0 +1,72 @@ +/** + * OpenTok Java SDK + * Copyright (C) 2023 Vonage. + * http://www.tokbox.com + * + * Licensed under The MIT License (MIT). See LICENSE file for more information. + */ +package com.opentok; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Defines valid video resolutions. + */ +public enum Resolution { + /** + * 480p landscape (640x480) + */ + SD_HORIZONTAL("640x480"), + + /** + * 480p portrait (480x640) + */ + SD_VERTICAL("480x640"), + + /** + * 720p landscape (1280x720) + */ + HD_HORIZONTAL("1280x720"), + + /** + * 720p portrait (720x1280) + */ + HD_VERTICAL("720x1280"), + + /** + * 1080p landscape (1920x1080) + */ + FHD_HORIZONTAL("1920x1080"), + + /** + * 1080p portrait (1080x1920) + */ + FHD_VERTICAL("1080x1920"); + + private static final Map RESOLUTION_INDEX = + Arrays.stream(Resolution.values()).collect(Collectors.toMap( + Resolution::toString, Function.identity() + )); + + private final String value; + + Resolution(String value) { + this.value = value; + } + + @JsonValue + @Override + public String toString() { + return value; + } + + @JsonCreator + public static Resolution fromString(String resolution) { + return RESOLUTION_INDEX.getOrDefault(resolution, null); + } +} diff --git a/src/main/java/com/opentok/SessionProperties.java b/src/main/java/com/opentok/SessionProperties.java index 55eac725..4ecb1b33 100644 --- a/src/main/java/com/opentok/SessionProperties.java +++ b/src/main/java/com/opentok/SessionProperties.java @@ -20,16 +20,19 @@ * @see OpenTok#createSession(com.opentok.SessionProperties properties) */ public class SessionProperties { - private String location; + private String location, archiveName; private MediaMode mediaMode; private ArchiveMode archiveMode; + private Resolution archiveResolution; private boolean e2ee; private SessionProperties(Builder builder) { - this.location = builder.location; - this.mediaMode = builder.mediaMode; - this.archiveMode = builder.archiveMode; - this.e2ee = builder.e2ee; + location = builder.location; + mediaMode = builder.mediaMode; + archiveMode = builder.archiveMode; + e2ee = builder.e2ee; + archiveName = builder.archiveName; + archiveResolution = builder.archiveResolution; } /** @@ -38,9 +41,10 @@ private SessionProperties(Builder builder) { * @see SessionProperties */ public static class Builder { - private String location; + private String location, archiveName; private MediaMode mediaMode = MediaMode.RELAYED; private ArchiveMode archiveMode = ArchiveMode.MANUAL; + private Resolution archiveResolution; private boolean e2ee = false; /** @@ -117,6 +121,32 @@ public Builder archiveMode(ArchiveMode archiveMode) { return this; } + /** + * Indicates the archive resolution for all the archives in auto archived session. A session that begins with + * archive mode {@link ArchiveMode#ALWAYS} will use this resolution for all archives of that session. + * + * @param archiveResolution The auto archive resolution as an enum. + * + * @return The SessionProperties.Builder object with the archive resolution setting. + */ + public Builder archiveResolution(Resolution archiveResolution) { + this.archiveResolution = archiveResolution; + return this; + } + + /** + * Indicates the archive name for all the archives in auto archived session. A session that begins with + * archive mode {@link ArchiveMode#ALWAYS} will use this archive name for all archives of that session. + * + * @param archiveName The archive name, maximum 80 characters in length. + * + * @return The SessionProperties.Builder object with the archive name setting. + */ + public Builder archiveName(String archiveName) { + this.archiveName = archiveName; + return this; + } + /** * Enables end-to-end encryption for a routed session. * You must also set {@link #mediaMode(MediaMode)} to {@linkplain MediaMode#ROUTED} when @@ -135,7 +165,7 @@ public Builder endToEndEncryption() { * @return The SessionProperties object. */ public SessionProperties build() { - if (this.archiveMode == ArchiveMode.ALWAYS && this.mediaMode != MediaMode.ROUTED) { + if (archiveMode == ArchiveMode.ALWAYS && mediaMode != MediaMode.ROUTED) { throw new IllegalStateException( "A session with ALWAYS archive mode must also have the ROUTED media mode." ); @@ -150,6 +180,17 @@ public SessionProperties build() { "A session with ALWAYS archive mode cannot have end-to-end encryption enabled." ); } + if (archiveMode == ArchiveMode.MANUAL) { + if (archiveResolution != null) { + throw new IllegalStateException("Resolution cannot be set for manual archives."); + } + if (archiveName != null) { + throw new IllegalStateException("Name cannot be set for manual archives."); + } + } + if (archiveName != null && (archiveName.trim().length() < 1 || archiveName.length() > 80)) { + throw new IllegalArgumentException("Archive name must be between 1 and 80 characters."); + } return new SessionProperties(this); } } @@ -179,11 +220,30 @@ public ArchiveMode archiveMode() { return archiveMode; } + /** + * Indicates the archive resolution for all the archives in auto archived session. A session that begins with + * archive mode {@link ArchiveMode#ALWAYS} will use this resolution for all archives of that session. + * + * @return The archive name, or {@code null} if not set (the default). + */ + public String archiveName() { + return archiveName; + } + + /** + * Indicates the archive resolution for all the archives in auto archived session. A session that begins with + * archive mode {@link ArchiveMode#ALWAYS} will use this resolution for all archives of that session. + * + * @return The archive resolution enum, or {@code null} if not set (the default). + */ + public Resolution archiveResolution() { + return archiveResolution; + } + /** * Defines whether the session will use * end-to-end encryption. * See {@link com.opentok.SessionProperties.Builder#endToEndEncryption()}. - * * * @return {@code true} if end-to-end encryption is enabled, {@code false} otherwise. */ @@ -192,23 +252,43 @@ public boolean isEndToEndEncrypted() { } /** - * Returns the session properties as a Map. + * Serializes the properties for making a request. + * + * @return The session properties as a Map. */ public Map> toMap() { Map> params = new HashMap<>(); - if (null != location) { + + if (location != null) { ArrayList valueList = new ArrayList<>(1); valueList.add(location); params.put("location", valueList); } - - ArrayList mediaModeValueList = new ArrayList<>(1); - mediaModeValueList.add(mediaMode.toString()); - params.put("p2p.preference", mediaModeValueList); - - ArrayList archiveModeValueList = new ArrayList<>(1); - archiveModeValueList.add(archiveMode.toString()); - params.put("archiveMode", archiveModeValueList); + if (mediaMode != null) { + ArrayList mediaModeValueList = new ArrayList<>(1); + mediaModeValueList.add(mediaMode.toString()); + params.put("p2p.preference", mediaModeValueList); + } + if (archiveMode != null) { + ArrayList archiveModeValueList = new ArrayList<>(1); + archiveModeValueList.add(archiveMode.toString()); + params.put("archiveMode", archiveModeValueList); + } + if (archiveResolution != null) { + ArrayList archiveResolutionValueList = new ArrayList<>(1); + archiveResolutionValueList.add(archiveResolution.toString()); + params.put("archiveResolution", archiveResolutionValueList); + } + if (archiveName != null) { + ArrayList archiveNameValueList = new ArrayList<>(1); + archiveNameValueList.add(archiveName); + params.put("archiveName", archiveNameValueList); + } + if (e2ee) { + ArrayList e2eeValueList = new ArrayList<>(1); + e2eeValueList.add(String.valueOf(e2ee)); + params.put("e2ee", e2eeValueList); + } if (e2ee) { ArrayList e2eeValueList = new ArrayList<>(1); diff --git a/src/main/java/com/opentok/constants/Version.java b/src/main/java/com/opentok/constants/Version.java index d0ff3692..ceb77535 100644 --- a/src/main/java/com/opentok/constants/Version.java +++ b/src/main/java/com/opentok/constants/Version.java @@ -8,5 +8,5 @@ package com.opentok.constants; public class Version { - public static final String VERSION = "4.10.0"; + public static final String VERSION = "4.11.0"; } diff --git a/src/main/java/com/opentok/util/HttpClient.java b/src/main/java/com/opentok/util/HttpClient.java index bd0bbc0a..bb058df3 100644 --- a/src/main/java/com/opentok/util/HttpClient.java +++ b/src/main/java/com/opentok/util/HttpClient.java @@ -492,6 +492,8 @@ public String startBroadcast(String sessionId, BroadcastProperties properties) t ObjectNode requestJson = nodeFactory.objectNode(); requestJson.put("sessionId", sessionId); requestJson.put("streamMode", properties.streamMode().toString()); + requestJson.put("hasAudio", properties.hasAudio()); + requestJson.put("hasVideo", properties.hasVideo()); if (properties.layout() != null) { ObjectNode layout = requestJson.putObject("layout"); @@ -560,7 +562,6 @@ public String startBroadcast(String sessionId, BroadcastProperties properties) t throw new RequestException("Could not start an OpenTok Broadcast. A bad request, check input properties like resolution etc."); case 403: throw new RequestException("Could not start an OpenTok Broadcast. The request was not authorized."); - case 409: throw new RequestException("The broadcast has already been started for the session. SessionId = " + sessionId); case 500: diff --git a/src/test/java/com/opentok/test/OpenTokTest.java b/src/test/java/com/opentok/test/OpenTokTest.java index 7cc7c1db..1ce59d57 100644 --- a/src/test/java/com/opentok/test/OpenTokTest.java +++ b/src/test/java/com/opentok/test/OpenTokTest.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; +import static com.github.tomakehurst.wiremock.client.WireMock.matching; import com.github.tomakehurst.wiremock.core.WireMockConfiguration; import com.github.tomakehurst.wiremock.junit.WireMockRule; import com.opentok.*; @@ -373,6 +374,8 @@ public void testCreateAlwaysArchivedSession() throws OpenTokException { SessionProperties properties = new SessionProperties.Builder() .archiveMode(ArchiveMode.ALWAYS) .mediaMode(MediaMode.ROUTED) + .archiveResolution(Resolution.HD_VERTICAL) + .archiveName("720pTest") .build(); Session session = sdk.createSession(properties); @@ -380,16 +383,48 @@ public void testCreateAlwaysArchivedSession() throws OpenTokException { assertEquals(apiKey, session.getApiKey()); assertEquals(sessionId, session.getSessionId()); assertEquals(ArchiveMode.ALWAYS, session.getProperties().archiveMode()); - + assertEquals(Resolution.HD_VERTICAL, session.getProperties().archiveResolution()); verify(postRequestedFor(urlMatching(SESSION_CREATE)) // TODO: this is a pretty bad way to verify, ideally we can decode the body and then query the object - .withRequestBody(matching(".*archiveMode=always.*"))); + .withRequestBody(matching(".*archiveMode=always.*")) + .withRequestBody(matching(".*archiveResolution=720x1280.*")) + .withRequestBody(matching(".*archiveName=720pTest.*"))); assertTrue(Helpers.verifyTokenAuth(apiKey, apiSecret, findAll(postRequestedFor(urlMatching(SESSION_CREATE))))); Helpers.verifyUserAgent(); } + @Test + public void testAutoArchiveSessionValidation() { + SessionProperties.Builder builder = new SessionProperties.Builder() + .archiveMode(ArchiveMode.ALWAYS) + .mediaMode(MediaMode.ROUTED); + + SessionProperties plain = builder.build(); + assertNull(plain.archiveName()); + assertNull(plain.archiveResolution()); + + assertEquals(1, builder.archiveName("A").build().archiveName().length()); + assertThrows(IllegalArgumentException.class, () -> builder.archiveName("").build()); + StringBuilder sb = new StringBuilder(80); + for (int i = 0; i < 10; sb.append("Archive").append(i++)); + assertEquals(80, builder.archiveName(sb.toString()).build().archiveName().length()); + assertThrows(IllegalArgumentException.class, () -> builder.archiveName(sb.append("N").toString()).build()); + + builder.archiveName("Test").archiveMode(ArchiveMode.MANUAL); + assertThrows(IllegalStateException.class, builder::build); + + SessionProperties fhd = builder + .archiveMode(ArchiveMode.ALWAYS) + .archiveResolution(Resolution.FHD_HORIZONTAL) + .archiveName(null).build(); + assertEquals("1920x1080", fhd.archiveResolution().toString()); + assertNull(fhd.archiveName()); + + assertThrows(IllegalStateException.class, () -> builder.archiveMode(ArchiveMode.MANUAL).build()); + } + @Test(expected = InvalidArgumentException.class) public void testCreateBadSession() throws OpenTokException { SessionProperties properties = new SessionProperties.Builder() @@ -1742,6 +1777,13 @@ public void testStartBroadcast() throws OpenTokException { String sessionId = "SESSIONID"; String url = "/v2/project/" + this.apiKey + "/broadcast"; stubFor(post(urlEqualTo(url)) + .withRequestBody(equalTo("{\"sessionId\":\"SESSIONID\",\"streamMode\":\"auto\"," + + "\"hasAudio\":false,\"hasVideo\":false,\"layout\":{\"type\":\"pip\"},\"maxDuration\":1000," + + "\"resolution\":\"1920x1080\",\"multiBroadcastTag\":\"MyVideoBroadcastTag\",\"outputs\":{" + + "\"hls\":{},\"rtmp\":[{\"id\":\"foo\",\"serverUrl\":\"rtmp://myfooserver/myfooapp\"," + + "\"streamName\":\"myfoostream\"},{\"id\":\"bar\",\"serverUrl\":" + + "\"rtmp://mybarserver/mybarapp\",\"streamName\":\"mybarstream\"}]}}" + )) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") @@ -1750,7 +1792,9 @@ public void testStartBroadcast() throws OpenTokException { " \"sessionId\" : \"SESSIONID\",\n" + " \"projectId\" : 123456,\n" + " \"createdAt\" : 1437676551000,\n" + - " \"upDatedAt\" : 1437676551000,\n" + + " \"updatedAt\" : 1437676551000,\n" + + " \"hasAudio\" : false,\n" + + " \"hasVideo\" : false,\n" + " \"resolution\" : \"1280x720\",\n" + " \"status\" : \"started\",\n" + " \"multiBroadcastTag\" : \"MyVideoBroadcastTag\",\n" + @@ -1779,7 +1823,8 @@ public void testStartBroadcast() throws OpenTokException { .addRtmpProperties(rtmpProps) .addRtmpProperties(rtmpNextProps) .maxDuration(1000) - .resolution("640x480") + .resolution("1920x1080") + .hasAudio(false).hasVideo(false) .multiBroadcastTag("MyVideoBroadcastTag") .layout(layout) .streamMode(Broadcast.StreamMode.AUTO) @@ -1794,6 +1839,8 @@ public void testStartBroadcast() throws OpenTokException { assertNotNull(rtmp.getStreamName()); assertNotNull(broadcast.toString()); assertNotNull(broadcast.getStatus()); + assertFalse(broadcast.hasAudio()); + assertFalse(broadcast.hasVideo()); assertEquals("1280x720", broadcast.getResolution()); assertTrue(broadcast.getCreatedAt() > 0); assertTrue(broadcast.getUpdatedAt() > -1); @@ -1901,7 +1948,7 @@ public void testBroadcastWithScreenShareType() throws OpenTokException { .maxDuration(5400) .layout(layout) .build(); - String expectedJson = String.format("{\"sessionId\":\"%s\",\"streamMode\":\"auto\",\"layout\":{\"type\":\"bestFit\",\"screenshareType\":\"pip\"},\"maxDuration\":5400,\"resolution\":\"640x480\",\"outputs\":{\"hls\":{},\"rtmp\":[]}}",sessionId); + String expectedJson = String.format("{\"sessionId\":\"%s\",\"streamMode\":\"auto\",\"hasAudio\":true,\"hasVideo\":true,\"layout\":{\"type\":\"bestFit\",\"screenshareType\":\"pip\"},\"maxDuration\":5400,\"resolution\":\"640x480\",\"outputs\":{\"hls\":{},\"rtmp\":[]}}",sessionId); Broadcast broadcast = sdk.startBroadcast(sessionId, properties); assertNotNull(broadcast); assertEquals(sessionId, broadcast.getSessionId());