Commit 43563997 by Jorge Ruesga

Enable embedded CEA-708 support

Currently, DashMediaPeriod only takes into account as CEA-608 accessibility tags as embedded
closed captions tracks CEA-608. CEA-708 closed captions format is parsed when is present on
its own AdaptationSet, but not when is embedded as an accessibility tag in a video AdaptaticonSet.

Embedded CEA-708 support is added by parsing accessibility tags like the example below:

  <Accessibility schemeIdUri="urn:scte:dash:cc:cea-708:2015" value="1=lang:eng;2=lang:deu"/>
  <Accessibility schemeIdUri="urn:scte:dash:cc:cea-708:2015" value="1=lang:eng;2=lang:eng,war:1,er:1"/>

so it creates a new CEA-708 track for accessibility tags with schemeIdUri = urn:scte:dash:cc:cea-708:2015
and extract accessibilityChannel and language from value attribute.

Signed-off-by: Jorge Ruesga <jorge@ruesga.com>
parent 535e14cb
...@@ -69,7 +69,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -69,7 +69,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
SequenceableLoader.Callback<ChunkSampleStream<DashChunkSource>>, SequenceableLoader.Callback<ChunkSampleStream<DashChunkSource>>,
ChunkSampleStream.ReleaseCallback<DashChunkSource> { ChunkSampleStream.ReleaseCallback<DashChunkSource> {
private enum CC_TYPE {
CEA608, CEA708
}
private static final Pattern CEA608_SERVICE_DESCRIPTOR_REGEX = Pattern.compile("CC([1-4])=(.+)"); private static final Pattern CEA608_SERVICE_DESCRIPTOR_REGEX = Pattern.compile("CC([1-4])=(.+)");
private static final Pattern CEA708_SERVICE_DESCRIPTOR_REGEX = Pattern.compile("([1-4])=lang:(\\w+)(,.+)?");
/* package */ final int id; /* package */ final int id;
private final DashChunkSource.Factory chunkSourceFactory; private final DashChunkSource.Factory chunkSourceFactory;
...@@ -488,14 +492,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -488,14 +492,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
int primaryGroupCount = groupedAdaptationSetIndices.length; int primaryGroupCount = groupedAdaptationSetIndices.length;
boolean[] primaryGroupHasEventMessageTrackFlags = new boolean[primaryGroupCount]; boolean[] primaryGroupHasEventMessageTrackFlags = new boolean[primaryGroupCount];
Format[][] primaryGroupCea608TrackFormats = new Format[primaryGroupCount][]; Format[][] primaryGroupClosedCaptionsTrackFormats = new Format[primaryGroupCount][];
int totalEmbeddedTrackGroupCount = int totalEmbeddedTrackGroupCount =
identifyEmbeddedTracks( identifyEmbeddedTracks(
primaryGroupCount, primaryGroupCount,
adaptationSets, adaptationSets,
groupedAdaptationSetIndices, groupedAdaptationSetIndices,
primaryGroupHasEventMessageTrackFlags, primaryGroupHasEventMessageTrackFlags,
primaryGroupCea608TrackFormats); primaryGroupClosedCaptionsTrackFormats);
int totalGroupCount = primaryGroupCount + totalEmbeddedTrackGroupCount + eventStreams.size(); int totalGroupCount = primaryGroupCount + totalEmbeddedTrackGroupCount + eventStreams.size();
TrackGroup[] trackGroups = new TrackGroup[totalGroupCount]; TrackGroup[] trackGroups = new TrackGroup[totalGroupCount];
...@@ -508,7 +512,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -508,7 +512,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
groupedAdaptationSetIndices, groupedAdaptationSetIndices,
primaryGroupCount, primaryGroupCount,
primaryGroupHasEventMessageTrackFlags, primaryGroupHasEventMessageTrackFlags,
primaryGroupCea608TrackFormats, primaryGroupClosedCaptionsTrackFormats,
trackGroups, trackGroups,
trackGroupInfos); trackGroupInfos);
...@@ -616,8 +620,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -616,8 +620,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
* same primary group, grouped in primary track groups order. * same primary group, grouped in primary track groups order.
* @param primaryGroupHasEventMessageTrackFlags An output array to be filled with flags indicating * @param primaryGroupHasEventMessageTrackFlags An output array to be filled with flags indicating
* whether each of the primary track groups contains an embedded event message track. * whether each of the primary track groups contains an embedded event message track.
* @param primaryGroupCea608TrackFormats An output array to be filled with track formats for * @param primaryGroupClosedCaptionsTrackFormats An output array to be filled with track formats for
* CEA-608 tracks embedded in each of the primary track groups. * closed captions tracks embedded in each of the primary track groups.
* @return Total number of embedded track groups. * @return Total number of embedded track groups.
*/ */
private static int identifyEmbeddedTracks( private static int identifyEmbeddedTracks(
...@@ -625,16 +629,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -625,16 +629,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
List<AdaptationSet> adaptationSets, List<AdaptationSet> adaptationSets,
int[][] groupedAdaptationSetIndices, int[][] groupedAdaptationSetIndices,
boolean[] primaryGroupHasEventMessageTrackFlags, boolean[] primaryGroupHasEventMessageTrackFlags,
Format[][] primaryGroupCea608TrackFormats) { Format[][] primaryGroupClosedCaptionsTrackFormats) {
int numEmbeddedTrackGroups = 0; int numEmbeddedTrackGroups = 0;
for (int i = 0; i < primaryGroupCount; i++) { for (int i = 0; i < primaryGroupCount; i++) {
if (hasEventMessageTrack(adaptationSets, groupedAdaptationSetIndices[i])) { if (hasEventMessageTrack(adaptationSets, groupedAdaptationSetIndices[i])) {
primaryGroupHasEventMessageTrackFlags[i] = true; primaryGroupHasEventMessageTrackFlags[i] = true;
numEmbeddedTrackGroups++; numEmbeddedTrackGroups++;
} }
primaryGroupCea608TrackFormats[i] = primaryGroupClosedCaptionsTrackFormats[i] =
getCea608TrackFormats(adaptationSets, groupedAdaptationSetIndices[i]); getClosedCaptionsTrackFormats(adaptationSets, groupedAdaptationSetIndices[i]);
if (primaryGroupCea608TrackFormats[i].length != 0) { if (primaryGroupClosedCaptionsTrackFormats[i].length != 0) {
numEmbeddedTrackGroups++; numEmbeddedTrackGroups++;
} }
} }
...@@ -647,7 +651,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -647,7 +651,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
int[][] groupedAdaptationSetIndices, int[][] groupedAdaptationSetIndices,
int primaryGroupCount, int primaryGroupCount,
boolean[] primaryGroupHasEventMessageTrackFlags, boolean[] primaryGroupHasEventMessageTrackFlags,
Format[][] primaryGroupCea608TrackFormats, Format[][] primaryGroupClosedCaptionsTrackFormats,
TrackGroup[] trackGroups, TrackGroup[] trackGroups,
TrackGroupInfo[] trackGroupInfos) { TrackGroupInfo[] trackGroupInfos) {
int trackGroupCount = 0; int trackGroupCount = 0;
...@@ -673,8 +677,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -673,8 +677,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
int primaryTrackGroupIndex = trackGroupCount++; int primaryTrackGroupIndex = trackGroupCount++;
int eventMessageTrackGroupIndex = int eventMessageTrackGroupIndex =
primaryGroupHasEventMessageTrackFlags[i] ? trackGroupCount++ : C.INDEX_UNSET; primaryGroupHasEventMessageTrackFlags[i] ? trackGroupCount++ : C.INDEX_UNSET;
int cea608TrackGroupIndex = int closedCaptionsTrackGroupIndex =
primaryGroupCea608TrackFormats[i].length != 0 ? trackGroupCount++ : C.INDEX_UNSET; primaryGroupClosedCaptionsTrackFormats[i].length != 0 ? trackGroupCount++ : C.INDEX_UNSET;
trackGroups[primaryTrackGroupIndex] = new TrackGroup(formats); trackGroups[primaryTrackGroupIndex] = new TrackGroup(formats);
trackGroupInfos[primaryTrackGroupIndex] = trackGroupInfos[primaryTrackGroupIndex] =
...@@ -683,7 +687,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -683,7 +687,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
adaptationSetIndices, adaptationSetIndices,
primaryTrackGroupIndex, primaryTrackGroupIndex,
eventMessageTrackGroupIndex, eventMessageTrackGroupIndex,
cea608TrackGroupIndex); closedCaptionsTrackGroupIndex);
if (eventMessageTrackGroupIndex != C.INDEX_UNSET) { if (eventMessageTrackGroupIndex != C.INDEX_UNSET) {
Format format = Format format =
new Format.Builder() new Format.Builder()
...@@ -694,10 +698,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -694,10 +698,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
trackGroupInfos[eventMessageTrackGroupIndex] = trackGroupInfos[eventMessageTrackGroupIndex] =
TrackGroupInfo.embeddedEmsgTrack(adaptationSetIndices, primaryTrackGroupIndex); TrackGroupInfo.embeddedEmsgTrack(adaptationSetIndices, primaryTrackGroupIndex);
} }
if (cea608TrackGroupIndex != C.INDEX_UNSET) { if (closedCaptionsTrackGroupIndex != C.INDEX_UNSET) {
trackGroups[cea608TrackGroupIndex] = new TrackGroup(primaryGroupCea608TrackFormats[i]); trackGroups[closedCaptionsTrackGroupIndex] =
trackGroupInfos[cea608TrackGroupIndex] = new TrackGroup(primaryGroupClosedCaptionsTrackFormats[i]);
TrackGroupInfo.embeddedCea608Track(adaptationSetIndices, primaryTrackGroupIndex); trackGroupInfos[closedCaptionsTrackGroupIndex] =
TrackGroupInfo.embeddedClosedCaptionsTrack(adaptationSetIndices, primaryTrackGroupIndex);
} }
} }
return trackGroupCount; return trackGroupCount;
...@@ -728,11 +733,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -728,11 +733,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
trackGroups.get(trackGroupInfo.embeddedEventMessageTrackGroupIndex); trackGroups.get(trackGroupInfo.embeddedEventMessageTrackGroupIndex);
embeddedTrackCount++; embeddedTrackCount++;
} }
boolean enableCea608Tracks = trackGroupInfo.embeddedCea608TrackGroupIndex != C.INDEX_UNSET; boolean enableClosedCaptionsTracks =
TrackGroup embeddedCea608TrackGroup = null; trackGroupInfo.embeddedClosedCaptionsTrackGroupIndex != C.INDEX_UNSET;
if (enableCea608Tracks) { TrackGroup embeddedClosedCaptionsTrackGroup = null;
embeddedCea608TrackGroup = trackGroups.get(trackGroupInfo.embeddedCea608TrackGroupIndex); if (enableClosedCaptionsTracks) {
embeddedTrackCount += embeddedCea608TrackGroup.length; embeddedClosedCaptionsTrackGroup =
trackGroups.get(trackGroupInfo.embeddedClosedCaptionsTrackGroupIndex);
embeddedTrackCount += embeddedClosedCaptionsTrackGroup.length;
} }
Format[] embeddedTrackFormats = new Format[embeddedTrackCount]; Format[] embeddedTrackFormats = new Format[embeddedTrackCount];
...@@ -743,12 +750,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -743,12 +750,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
embeddedTrackTypes[embeddedTrackCount] = C.TRACK_TYPE_METADATA; embeddedTrackTypes[embeddedTrackCount] = C.TRACK_TYPE_METADATA;
embeddedTrackCount++; embeddedTrackCount++;
} }
List<Format> embeddedCea608TrackFormats = new ArrayList<>(); List<Format> embeddedClosedCaptionsTrackFormats = new ArrayList<>();
if (enableCea608Tracks) { if (enableClosedCaptionsTracks) {
for (int i = 0; i < embeddedCea608TrackGroup.length; i++) { for (int i = 0; i < embeddedClosedCaptionsTrackGroup.length; i++) {
embeddedTrackFormats[embeddedTrackCount] = embeddedCea608TrackGroup.getFormat(i); embeddedTrackFormats[embeddedTrackCount] = embeddedClosedCaptionsTrackGroup.getFormat(i);
embeddedTrackTypes[embeddedTrackCount] = C.TRACK_TYPE_TEXT; embeddedTrackTypes[embeddedTrackCount] = C.TRACK_TYPE_TEXT;
embeddedCea608TrackFormats.add(embeddedTrackFormats[embeddedTrackCount]); embeddedClosedCaptionsTrackFormats.add(embeddedTrackFormats[embeddedTrackCount]);
embeddedTrackCount++; embeddedTrackCount++;
} }
} }
...@@ -767,7 +774,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -767,7 +774,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
trackGroupInfo.trackType, trackGroupInfo.trackType,
elapsedRealtimeOffsetMs, elapsedRealtimeOffsetMs,
enableEventMessageTrack, enableEventMessageTrack,
embeddedCea608TrackFormats, embeddedClosedCaptionsTrackFormats,
trackPlayerEmsgHandler, trackPlayerEmsgHandler,
transferListener); transferListener);
ChunkSampleStream<DashChunkSource> stream = ChunkSampleStream<DashChunkSource> stream =
...@@ -824,59 +831,96 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -824,59 +831,96 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return false; return false;
} }
private static Format[] getCea608TrackFormats( private static Format[] getClosedCaptionsTrackFormats(
List<AdaptationSet> adaptationSets, int[] adaptationSetIndices) { List<AdaptationSet> adaptationSets, int[] adaptationSetIndices) {
for (int i : adaptationSetIndices) { for (int i : adaptationSetIndices) {
AdaptationSet adaptationSet = adaptationSets.get(i); AdaptationSet adaptationSet = adaptationSets.get(i);
List<Descriptor> descriptors = adaptationSets.get(i).accessibilityDescriptors; List<Descriptor> descriptors = adaptationSets.get(i).accessibilityDescriptors;
for (int j = 0; j < descriptors.size(); j++) { for (Descriptor descriptor : descriptors) {
Descriptor descriptor = descriptors.get(j); return parseClosedCaptionsDescriptor(adaptationSet.id, descriptor);
if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)) {
@Nullable String value = descriptor.value;
if (value == null) {
// There are embedded CEA-608 tracks, but service information is not declared.
return new Format[] {buildCea608TrackFormat(adaptationSet.id)};
}
String[] services = Util.split(value, ";");
Format[] formats = new Format[services.length];
for (int k = 0; k < services.length; k++) {
Matcher matcher = CEA608_SERVICE_DESCRIPTOR_REGEX.matcher(services[k]);
if (!matcher.matches()) {
// If we can't parse service information for all services, assume a single track.
return new Format[] {buildCea608TrackFormat(adaptationSet.id)};
}
formats[k] =
buildCea608TrackFormat(
adaptationSet.id,
/* language= */ matcher.group(2),
/* accessibilityChannel= */ Integer.parseInt(matcher.group(1)));
}
return formats;
}
} }
} }
return new Format[0]; return new Format[0];
} }
private static Format buildCea608TrackFormat(int adaptationSetId) { private static Format[] parseClosedCaptionsDescriptor(int adaptationSetId, Descriptor descriptor) {
return buildCea608TrackFormat( if ("urn:scte:dash:cc:cea-708:2015".equals(descriptor.schemeIdUri)) {
adaptationSetId, /* language= */ null, /* accessibilityChannel= */ Format.NO_VALUE); return parseClosedCaptionsDescriptor(adaptationSetId, CC_TYPE.CEA708, descriptor);
} else if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)) {
return parseClosedCaptionsDescriptor(adaptationSetId, CC_TYPE.CEA608, descriptor);
}
return null;
} }
private static Format buildCea608TrackFormat( private static Format[] parseClosedCaptionsDescriptor(
int adaptationSetId, @Nullable String language, int accessibilityChannel) { int adaptationSetId, CC_TYPE type, Descriptor descriptor) {
String value = descriptor.value;
if (value == null) {
// There are embedded closed captions tracks, but service information is not declared.
return new Format[] {buildClosedCaptionsTrackFormat(adaptationSetId, type)};
}
String[] services = Util.split(value, ";");
Format[] formats = new Format[services.length];
for (int k = 0; k < services.length; k++) {
Pattern pattern = getClosedCaptionsDescriptorRegex(type);
if (pattern == null) {
// If we can't parse service information for all services, assume a single track.
return new Format[] {buildClosedCaptionsTrackFormat(adaptationSetId, type)};
}
Matcher matcher = pattern.matcher(services[k]);
if (!matcher.matches()) {
// If we can't parse service information for all services, assume a single track.
return new Format[] {buildClosedCaptionsTrackFormat(adaptationSetId, type)};
}
formats[k] =
buildClosedCaptionsTrackFormat(
adaptationSetId,
type,
/* language= */ matcher.group(2),
/* accessibilityChannel= */ Integer.parseInt(matcher.group(1)));
}
return formats;
}
private static Format buildClosedCaptionsTrackFormat(int adaptationSetId, CC_TYPE type) {
return buildClosedCaptionsTrackFormat(
adaptationSetId, type, /* language= */ null, /* accessibilityChannel= */ Format.NO_VALUE);
}
private static Format buildClosedCaptionsTrackFormat(
int adaptationSetId, CC_TYPE type, @Nullable String language, int accessibilityChannel) {
String id = String id =
adaptationSetId adaptationSetId
+ ":cea608" + ":" + type.toString().toLowerCase()
+ (accessibilityChannel != Format.NO_VALUE ? ":" + accessibilityChannel : ""); + (accessibilityChannel != Format.NO_VALUE ? ":" + accessibilityChannel : "");
return new Format.Builder() return new Format.Builder()
.setId(id) .setId(id)
.setSampleMimeType(MimeTypes.APPLICATION_CEA608) .setSampleMimeType(getClosedCaptionsMimeType(type))
.setLanguage(language) .setLanguage(language)
.setAccessibilityChannel(accessibilityChannel) .setAccessibilityChannel(accessibilityChannel)
.build(); .build();
} }
private static String getClosedCaptionsMimeType(CC_TYPE type) {
switch (type) {
case CEA608:
return MimeTypes.APPLICATION_CEA608;
case CEA708:
return MimeTypes.APPLICATION_CEA708;
}
return null;
}
private static Pattern getClosedCaptionsDescriptorRegex(CC_TYPE type) {
switch (type) {
case CEA608:
return CEA608_SERVICE_DESCRIPTOR_REGEX;
case CEA708:
return CEA708_SERVICE_DESCRIPTOR_REGEX;
}
return null;
}
// We won't assign the array to a variable that erases the generic type, and then write into it. // We won't assign the array to a variable that erases the generic type, and then write into it.
@SuppressWarnings({"unchecked", "rawtypes"}) @SuppressWarnings({"unchecked", "rawtypes"})
private static ChunkSampleStream<DashChunkSource>[] newSampleStreamArray(int length) { private static ChunkSampleStream<DashChunkSource>[] newSampleStreamArray(int length) {
...@@ -916,21 +960,21 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -916,21 +960,21 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
public final int eventStreamGroupIndex; public final int eventStreamGroupIndex;
public final int primaryTrackGroupIndex; public final int primaryTrackGroupIndex;
public final int embeddedEventMessageTrackGroupIndex; public final int embeddedEventMessageTrackGroupIndex;
public final int embeddedCea608TrackGroupIndex; public final int embeddedClosedCaptionsTrackGroupIndex;
public static TrackGroupInfo primaryTrack( public static TrackGroupInfo primaryTrack(
int trackType, int trackType,
int[] adaptationSetIndices, int[] adaptationSetIndices,
int primaryTrackGroupIndex, int primaryTrackGroupIndex,
int embeddedEventMessageTrackGroupIndex, int embeddedEventMessageTrackGroupIndex,
int embeddedCea608TrackGroupIndex) { int embeddedClosedCaptionsTrackGroupIndex) {
return new TrackGroupInfo( return new TrackGroupInfo(
trackType, trackType,
CATEGORY_PRIMARY, CATEGORY_PRIMARY,
adaptationSetIndices, adaptationSetIndices,
primaryTrackGroupIndex, primaryTrackGroupIndex,
embeddedEventMessageTrackGroupIndex, embeddedEventMessageTrackGroupIndex,
embeddedCea608TrackGroupIndex, embeddedClosedCaptionsTrackGroupIndex,
/* eventStreamGroupIndex= */ -1); /* eventStreamGroupIndex= */ -1);
} }
...@@ -946,7 +990,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -946,7 +990,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/* eventStreamGroupIndex= */ -1); /* eventStreamGroupIndex= */ -1);
} }
public static TrackGroupInfo embeddedCea608Track(int[] adaptationSetIndices, public static TrackGroupInfo embeddedClosedCaptionsTrack(int[] adaptationSetIndices,
int primaryTrackGroupIndex) { int primaryTrackGroupIndex) {
return new TrackGroupInfo( return new TrackGroupInfo(
C.TRACK_TYPE_TEXT, C.TRACK_TYPE_TEXT,
...@@ -975,14 +1019,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -975,14 +1019,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
int[] adaptationSetIndices, int[] adaptationSetIndices,
int primaryTrackGroupIndex, int primaryTrackGroupIndex,
int embeddedEventMessageTrackGroupIndex, int embeddedEventMessageTrackGroupIndex,
int embeddedCea608TrackGroupIndex, int embeddedClosedCaptionsTrackGroupIndex,
int eventStreamGroupIndex) { int eventStreamGroupIndex) {
this.trackType = trackType; this.trackType = trackType;
this.adaptationSetIndices = adaptationSetIndices; this.adaptationSetIndices = adaptationSetIndices;
this.trackGroupCategory = trackGroupCategory; this.trackGroupCategory = trackGroupCategory;
this.primaryTrackGroupIndex = primaryTrackGroupIndex; this.primaryTrackGroupIndex = primaryTrackGroupIndex;
this.embeddedEventMessageTrackGroupIndex = embeddedEventMessageTrackGroupIndex; this.embeddedEventMessageTrackGroupIndex = embeddedEventMessageTrackGroupIndex;
this.embeddedCea608TrackGroupIndex = embeddedCea608TrackGroupIndex; this.embeddedClosedCaptionsTrackGroupIndex = embeddedClosedCaptionsTrackGroupIndex;
this.eventStreamGroupIndex = eventStreamGroupIndex; this.eventStreamGroupIndex = eventStreamGroupIndex;
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment