mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Add mpv profile
This commit is contained in:
parent
0597e93178
commit
fd6cdd78f3
4 changed files with 220 additions and 106 deletions
|
|
@ -55,6 +55,7 @@ import com.github.damontecres.wholphin.util.TrackSupport
|
||||||
import com.github.damontecres.wholphin.util.checkForSupport
|
import com.github.damontecres.wholphin.util.checkForSupport
|
||||||
import com.github.damontecres.wholphin.util.formatDateTime
|
import com.github.damontecres.wholphin.util.formatDateTime
|
||||||
import com.github.damontecres.wholphin.util.mpv.MpvPlayer
|
import com.github.damontecres.wholphin.util.mpv.MpvPlayer
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.mpvDeviceProfile
|
||||||
import com.github.damontecres.wholphin.util.seasonEpisodePadded
|
import com.github.damontecres.wholphin.util.seasonEpisodePadded
|
||||||
import com.github.damontecres.wholphin.util.subtitleMimeTypes
|
import com.github.damontecres.wholphin.util.subtitleMimeTypes
|
||||||
import com.github.damontecres.wholphin.util.supportItemKinds
|
import com.github.damontecres.wholphin.util.supportItemKinds
|
||||||
|
|
@ -434,50 +435,50 @@ class PlaybackViewModel
|
||||||
Timber.v("changeStreams direct play")
|
Timber.v("changeStreams direct play")
|
||||||
|
|
||||||
val source = currentPlayback.mediaSourceInfo
|
val source = currentPlayback.mediaSourceInfo
|
||||||
val externalSubtitleCount = source.externalSubtitlesCount
|
|
||||||
val externalSubtitle = source.findExternalSubtitle(subtitleIndex)
|
val externalSubtitle = source.findExternalSubtitle(subtitleIndex)
|
||||||
|
|
||||||
val result =
|
if (externalSubtitle == null) {
|
||||||
withContext(Dispatchers.Main) {
|
val result =
|
||||||
applyTrackSelections(
|
withContext(Dispatchers.Main) {
|
||||||
player,
|
applyTrackSelections(
|
||||||
playerBackend,
|
player,
|
||||||
true,
|
playerBackend,
|
||||||
audioIndex,
|
true,
|
||||||
subtitleIndex,
|
audioIndex,
|
||||||
source.videoStreamCount,
|
subtitleIndex,
|
||||||
source.audioStreamCount,
|
source,
|
||||||
externalSubtitleCount,
|
)
|
||||||
externalSubtitle != null,
|
}
|
||||||
)
|
if (result.bothSelected) {
|
||||||
}
|
// TODO lots of duplicate code in this block
|
||||||
if (result.bothSelected) {
|
Timber.d("Changes tracks audio=$audioIndex, subtitle=$subtitleIndex")
|
||||||
// TODO lots of duplicate code in this block
|
val itemPlayback =
|
||||||
Timber.d("Changes tracks audio=$audioIndex, subtitle=$subtitleIndex")
|
currentItemPlayback.copy(
|
||||||
val itemPlayback =
|
sourceId = source.id?.toUUIDOrNull(),
|
||||||
currentItemPlayback.copy(
|
audioIndex = audioIndex ?: TrackIndex.UNSPECIFIED,
|
||||||
sourceId = source.id?.toUUIDOrNull(),
|
subtitleIndex = subtitleIndex ?: TrackIndex.DISABLED,
|
||||||
audioIndex = audioIndex ?: TrackIndex.UNSPECIFIED,
|
)
|
||||||
subtitleIndex = subtitleIndex ?: TrackIndex.DISABLED,
|
if (userInitiated) {
|
||||||
)
|
viewModelScope.launchIO {
|
||||||
if (userInitiated) {
|
Timber.v("Saving user initiated item playback: %s", itemPlayback)
|
||||||
viewModelScope.launchIO {
|
val updated = itemPlaybackRepository.saveItemPlayback(itemPlayback)
|
||||||
Timber.v("Saving user initiated item playback: %s", itemPlayback)
|
withContext(Dispatchers.Main) {
|
||||||
val updated = itemPlaybackRepository.saveItemPlayback(itemPlayback)
|
this@PlaybackViewModel.currentItemPlayback.value = updated
|
||||||
withContext(Dispatchers.Main) {
|
}
|
||||||
this@PlaybackViewModel.currentItemPlayback.value = updated
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
withContext(Dispatchers.Main) {
|
||||||
withContext(Dispatchers.Main) {
|
this@PlaybackViewModel.currentPlayback.value =
|
||||||
this@PlaybackViewModel.currentPlayback.value =
|
currentPlayback.copy(
|
||||||
currentPlayback.copy(
|
tracks = checkForSupport(player.currentTracks),
|
||||||
tracks = checkForSupport(player.currentTracks),
|
)
|
||||||
)
|
this@PlaybackViewModel.currentItemPlayback.value = itemPlayback
|
||||||
this@PlaybackViewModel.currentItemPlayback.value = itemPlayback
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return@withContext
|
return@withContext
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.v("changeStreams direct play, external subtitle was requested")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -509,7 +510,7 @@ class PlaybackViewModel
|
||||||
if (playerBackend == PlayerBackend.EXO_PLAYER) {
|
if (playerBackend == PlayerBackend.EXO_PLAYER) {
|
||||||
deviceProfile
|
deviceProfile
|
||||||
} else {
|
} else {
|
||||||
null
|
mpvDeviceProfile
|
||||||
},
|
},
|
||||||
maxAudioChannels = null,
|
maxAudioChannels = null,
|
||||||
audioStreamIndex = audioIndex,
|
audioStreamIndex = audioIndex,
|
||||||
|
|
@ -581,7 +582,7 @@ class PlaybackViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.v("externalSubtitleCount=$externalSubtitleCount, externalSubtitle=$externalSubtitle")
|
Timber.v("subtitleIndex=$subtitleIndex, externalSubtitleCount=$externalSubtitleCount, externalSubtitle=$externalSubtitle")
|
||||||
|
|
||||||
val mediaItem =
|
val mediaItem =
|
||||||
MediaItem
|
MediaItem
|
||||||
|
|
@ -648,18 +649,18 @@ class PlaybackViewModel
|
||||||
override fun onTracksChanged(tracks: Tracks) {
|
override fun onTracksChanged(tracks: Tracks) {
|
||||||
Timber.v("onTracksChanged: $tracks")
|
Timber.v("onTracksChanged: $tracks")
|
||||||
if (tracks.groups.isNotEmpty()) {
|
if (tracks.groups.isNotEmpty()) {
|
||||||
applyTrackSelections(
|
val result =
|
||||||
player,
|
applyTrackSelections(
|
||||||
playerBackend,
|
player,
|
||||||
source.supportsDirectPlay,
|
playerBackend,
|
||||||
audioIndex,
|
source.supportsDirectPlay,
|
||||||
subtitleIndex,
|
audioIndex,
|
||||||
source.videoStreamCount,
|
subtitleIndex,
|
||||||
source.audioStreamCount,
|
source,
|
||||||
externalSubtitleCount,
|
)
|
||||||
externalSubtitle != null,
|
if (result.bothSelected) {
|
||||||
)
|
player.removeListener(this)
|
||||||
player.removeListener(this)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1152,6 +1153,14 @@ val MediaSourceInfo.externalSubtitlesCount: Int
|
||||||
mediaStreams
|
mediaStreams
|
||||||
?.count { it.type == MediaStreamType.SUBTITLE && it.isExternal } ?: 0
|
?.count { it.type == MediaStreamType.SUBTITLE && it.isExternal } ?: 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of embedded subtitle streams there are
|
||||||
|
*/
|
||||||
|
val MediaSourceInfo.embeddedSubtitleCount: Int
|
||||||
|
get() =
|
||||||
|
mediaStreams
|
||||||
|
?.count { it.type == MediaStreamType.SUBTITLE && !it.isExternal } ?: 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the number of video streams there are
|
* Returns the number of video streams there are
|
||||||
*/
|
*/
|
||||||
|
|
@ -1193,58 +1202,67 @@ private fun applyTrackSelections(
|
||||||
supportsDirectPlay: Boolean,
|
supportsDirectPlay: Boolean,
|
||||||
audioIndex: Int?,
|
audioIndex: Int?,
|
||||||
subtitleIndex: Int?,
|
subtitleIndex: Int?,
|
||||||
videoStreamCount: Int,
|
source: MediaSourceInfo,
|
||||||
audioStreamCount: Int,
|
|
||||||
externalSubtitleCount: Int,
|
|
||||||
subtitleIsExternal: Boolean,
|
|
||||||
): TrackSelectionResult {
|
): TrackSelectionResult {
|
||||||
val subtitleSelected =
|
val videoStreamCount = source.videoStreamCount
|
||||||
if (subtitleIndex != null && subtitleIndex >= 0 && (subtitleIsExternal || supportsDirectPlay)) {
|
val audioStreamCount = source.audioStreamCount
|
||||||
val chosenTrack =
|
val embeddedSubtitleCount = source.embeddedSubtitleCount
|
||||||
if (subtitleIsExternal) {
|
val externalSubtitleCount = source.externalSubtitlesCount
|
||||||
player.currentTracks.groups.firstOrNull { group ->
|
|
||||||
group.type == C.TRACK_TYPE_TEXT && group.isSupported &&
|
|
||||||
(0..<group.mediaTrackGroup.length)
|
|
||||||
.mapNotNull {
|
|
||||||
group.getTrackFormat(it).id
|
|
||||||
}.any { it.endsWith("e:$subtitleIndex") }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// TODO this might be incorrect
|
|
||||||
val indexToFind =
|
|
||||||
calculateIndexToFind(
|
|
||||||
subtitleIndex,
|
|
||||||
MediaStreamType.SUBTITLE,
|
|
||||||
playerBackend,
|
|
||||||
videoStreamCount,
|
|
||||||
audioStreamCount,
|
|
||||||
externalSubtitleCount,
|
|
||||||
)
|
|
||||||
Timber.v("Chosen subtitle ($subtitleIndex/$indexToFind) track")
|
|
||||||
// subtitleIndex - externalSubtitleCount + 1
|
|
||||||
player.currentTracks.groups.firstOrNull { group ->
|
|
||||||
group.type == C.TRACK_TYPE_TEXT && group.isSupported &&
|
|
||||||
(0..<group.mediaTrackGroup.length)
|
|
||||||
.map {
|
|
||||||
group.getTrackFormat(it).idAsInt
|
|
||||||
}.contains(indexToFind)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.v("Chosen subtitle ($subtitleIndex) track: $chosenTrack")
|
val subtitleSelected =
|
||||||
chosenTrack?.let {
|
if (subtitleIndex != null && subtitleIndex >= 0) {
|
||||||
player.trackSelectionParameters =
|
val subtitleIsExternal = source.findExternalSubtitle(subtitleIndex) != null
|
||||||
player.trackSelectionParameters
|
if (subtitleIsExternal || supportsDirectPlay) {
|
||||||
.buildUpon()
|
val chosenTrack =
|
||||||
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false)
|
if (subtitleIsExternal && playerBackend == PlayerBackend.EXO_PLAYER) {
|
||||||
.setOverrideForType(
|
player.currentTracks.groups.firstOrNull { group ->
|
||||||
TrackSelectionOverride(
|
group.type == C.TRACK_TYPE_TEXT && group.isSupported &&
|
||||||
chosenTrack.mediaTrackGroup,
|
(0..<group.mediaTrackGroup.length)
|
||||||
0,
|
.mapNotNull {
|
||||||
),
|
group.getTrackFormat(it).id
|
||||||
).build()
|
}.any { it.endsWith("e:$subtitleIndex") }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val indexToFind =
|
||||||
|
calculateIndexToFind(
|
||||||
|
subtitleIndex,
|
||||||
|
MediaStreamType.SUBTITLE,
|
||||||
|
playerBackend,
|
||||||
|
videoStreamCount,
|
||||||
|
audioStreamCount,
|
||||||
|
embeddedSubtitleCount,
|
||||||
|
externalSubtitleCount,
|
||||||
|
subtitleIsExternal,
|
||||||
|
)
|
||||||
|
Timber.v("Chosen subtitle ($subtitleIndex/$indexToFind) track")
|
||||||
|
// subtitleIndex - externalSubtitleCount + 1
|
||||||
|
val tracks = player.currentTracks.groups
|
||||||
|
tracks.firstOrNull { group ->
|
||||||
|
group.type == C.TRACK_TYPE_TEXT && group.isSupported &&
|
||||||
|
(0..<group.mediaTrackGroup.length)
|
||||||
|
.map {
|
||||||
|
group.getTrackFormat(it).idAsInt
|
||||||
|
}.contains(indexToFind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.v("Chosen subtitle ($subtitleIndex) track: $chosenTrack")
|
||||||
|
chosenTrack?.let {
|
||||||
|
player.trackSelectionParameters =
|
||||||
|
player.trackSelectionParameters
|
||||||
|
.buildUpon()
|
||||||
|
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false)
|
||||||
|
.setOverrideForType(
|
||||||
|
TrackSelectionOverride(
|
||||||
|
chosenTrack.mediaTrackGroup,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
chosenTrack != null
|
||||||
|
} else {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
chosenTrack != null
|
|
||||||
} else {
|
} else {
|
||||||
player.trackSelectionParameters =
|
player.trackSelectionParameters =
|
||||||
player.trackSelectionParameters
|
player.trackSelectionParameters
|
||||||
|
|
@ -1262,7 +1280,9 @@ private fun applyTrackSelections(
|
||||||
playerBackend,
|
playerBackend,
|
||||||
videoStreamCount,
|
videoStreamCount,
|
||||||
audioStreamCount,
|
audioStreamCount,
|
||||||
|
embeddedSubtitleCount,
|
||||||
externalSubtitleCount,
|
externalSubtitleCount,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
val chosenTrack =
|
val chosenTrack =
|
||||||
player.currentTracks.groups.firstOrNull { group ->
|
player.currentTracks.groups.firstOrNull { group ->
|
||||||
|
|
@ -1301,7 +1321,9 @@ private fun calculateIndexToFind(
|
||||||
playerBackend: PlayerBackend,
|
playerBackend: PlayerBackend,
|
||||||
videoStreamCount: Int,
|
videoStreamCount: Int,
|
||||||
audioStreamCount: Int,
|
audioStreamCount: Int,
|
||||||
|
embeddedSubtitleCount: Int,
|
||||||
externalSubtitleCount: Int,
|
externalSubtitleCount: Int,
|
||||||
|
subtitleIsExternal: Boolean,
|
||||||
): Int =
|
): Int =
|
||||||
when (playerBackend) {
|
when (playerBackend) {
|
||||||
PlayerBackend.EXO_PLAYER,
|
PlayerBackend.EXO_PLAYER,
|
||||||
|
|
@ -1315,7 +1337,13 @@ private fun calculateIndexToFind(
|
||||||
when (type) {
|
when (type) {
|
||||||
MediaStreamType.VIDEO -> serverIndex - externalSubtitleCount + 1
|
MediaStreamType.VIDEO -> serverIndex - externalSubtitleCount + 1
|
||||||
MediaStreamType.AUDIO -> serverIndex - externalSubtitleCount - videoStreamCount + 1
|
MediaStreamType.AUDIO -> serverIndex - externalSubtitleCount - videoStreamCount + 1
|
||||||
MediaStreamType.SUBTITLE -> serverIndex - externalSubtitleCount - videoStreamCount - audioStreamCount + 1
|
MediaStreamType.SUBTITLE -> {
|
||||||
|
if (subtitleIsExternal) {
|
||||||
|
serverIndex + embeddedSubtitleCount + 1
|
||||||
|
} else {
|
||||||
|
serverIndex - externalSubtitleCount - videoStreamCount - audioStreamCount + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> throw UnsupportedOperationException("Cannot calculate index for $type")
|
else -> throw UnsupportedOperationException("Cannot calculate index for $type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
package com.github.damontecres.wholphin.util.mpv
|
||||||
|
|
||||||
|
import com.github.damontecres.wholphin.util.profile.Codec
|
||||||
|
import com.github.damontecres.wholphin.util.profile.subtitleProfile
|
||||||
|
import com.github.damontecres.wholphin.util.profile.supportedAudioCodecs
|
||||||
|
import org.jellyfin.sdk.model.api.DlnaProfileType
|
||||||
|
import org.jellyfin.sdk.model.api.EncodingContext
|
||||||
|
import org.jellyfin.sdk.model.api.MediaStreamProtocol
|
||||||
|
import org.jellyfin.sdk.model.deviceprofile.buildDeviceProfile
|
||||||
|
|
||||||
|
val mpvDeviceProfile =
|
||||||
|
buildDeviceProfile {
|
||||||
|
name = "mpv"
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// maxStaticBitrate = maxBitrate
|
||||||
|
// maxStreamingBitrate = maxBitrate
|
||||||
|
transcodingProfile {
|
||||||
|
type = DlnaProfileType.VIDEO
|
||||||
|
context = EncodingContext.STREAMING
|
||||||
|
|
||||||
|
container = Codec.Container.TS
|
||||||
|
protocol = MediaStreamProtocol.HLS
|
||||||
|
|
||||||
|
videoCodec(Codec.Video.HEVC)
|
||||||
|
videoCodec(Codec.Video.H264)
|
||||||
|
|
||||||
|
audioCodec(*supportedAudioCodecs)
|
||||||
|
|
||||||
|
copyTimestamps = false
|
||||||
|
enableSubtitlesInManifest = true
|
||||||
|
}
|
||||||
|
directPlayProfile {
|
||||||
|
type = DlnaProfileType.VIDEO
|
||||||
|
|
||||||
|
container(
|
||||||
|
Codec.Container.ASF,
|
||||||
|
Codec.Container.AVI,
|
||||||
|
Codec.Container.HLS,
|
||||||
|
Codec.Container.M4V,
|
||||||
|
Codec.Container.MPEG,
|
||||||
|
Codec.Container.MPEGTS,
|
||||||
|
Codec.Container.MKV,
|
||||||
|
Codec.Container.MOV,
|
||||||
|
Codec.Container.MP4,
|
||||||
|
Codec.Container.OGM,
|
||||||
|
Codec.Container.OGV,
|
||||||
|
Codec.Container.TS,
|
||||||
|
Codec.Container.VOB,
|
||||||
|
Codec.Container.WEBM,
|
||||||
|
Codec.Container.WMV,
|
||||||
|
Codec.Container.XVID,
|
||||||
|
)
|
||||||
|
|
||||||
|
videoCodec(
|
||||||
|
Codec.Video.AV1,
|
||||||
|
Codec.Video.H264,
|
||||||
|
Codec.Video.HEVC,
|
||||||
|
Codec.Video.MPEG,
|
||||||
|
Codec.Video.MPEG2VIDEO,
|
||||||
|
Codec.Video.VP8,
|
||||||
|
Codec.Video.VP9,
|
||||||
|
)
|
||||||
|
|
||||||
|
audioCodec(*supportedAudioCodecs)
|
||||||
|
}
|
||||||
|
|
||||||
|
subtitleProfile(Codec.Subtitle.VTT, embedded = true, hls = true, external = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.WEBVTT, embedded = true, hls = true, external = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.SRT, embedded = true, external = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.SUBRIP, embedded = true, external = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.TTML, embedded = true, external = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.DVBSUB, embedded = true, encode = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.IDX, embedded = true, encode = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.PGS, embedded = true, encode = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.PGSSUB, embedded = true, encode = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.ASS, encode = true, embedded = true, external = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.SSA, encode = true, embedded = true, external = true)
|
||||||
|
}
|
||||||
|
|
@ -576,6 +576,11 @@ class MpvPlayer(
|
||||||
Timber.d("Seeking to $startPositionMs")
|
Timber.d("Seeking to $startPositionMs")
|
||||||
seekTo(startPositionMs)
|
seekTo(startPositionMs)
|
||||||
}
|
}
|
||||||
|
mediaItem!!.localConfiguration?.subtitleConfigurations?.forEach {
|
||||||
|
val url = it.uri.toString()
|
||||||
|
Timber.v("Adding external subtitle track $url")
|
||||||
|
MPVLib.command(arrayOf("sub-add", url, "auto"))
|
||||||
|
}
|
||||||
getTracks().let {
|
getTracks().let {
|
||||||
notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) }
|
notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) }
|
||||||
}
|
}
|
||||||
|
|
@ -675,7 +680,9 @@ class MpvPlayer(
|
||||||
.setCodecs(codec)
|
.setCodecs(codec)
|
||||||
.setSampleMimeType(mimeType)
|
.setSampleMimeType(mimeType)
|
||||||
.setLanguage(lang)
|
.setLanguage(lang)
|
||||||
.setLabel(listOfNotNull(title, codecDescription).joinToString(","))
|
// TODO title contains apikey for external subtitles
|
||||||
|
// .setLabel(listOfNotNull(title, codecDescription).joinToString(","))
|
||||||
|
.setLabel(codecDescription)
|
||||||
.setSelectionFlags(flags)
|
.setSelectionFlags(flags)
|
||||||
channelCount?.let(builder::setChannelCount)
|
channelCount?.let(builder::setChannelCount)
|
||||||
val format = builder.build()
|
val format = builder.build()
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ private val downmixSupportedAudioCodecs =
|
||||||
Codec.Audio.MP3,
|
Codec.Audio.MP3,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val supportedAudioCodecs =
|
val supportedAudioCodecs =
|
||||||
arrayOf(
|
arrayOf(
|
||||||
Codec.Audio.AAC,
|
Codec.Audio.AAC,
|
||||||
Codec.Audio.AAC_LATM,
|
Codec.Audio.AAC_LATM,
|
||||||
|
|
@ -495,7 +495,7 @@ fun createDeviceProfile(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Little helper function to more easily define subtitle profiles
|
// Little helper function to more easily define subtitle profiles
|
||||||
private fun DeviceProfileBuilder.subtitleProfile(
|
fun DeviceProfileBuilder.subtitleProfile(
|
||||||
format: String,
|
format: String,
|
||||||
embedded: Boolean = false,
|
embedded: Boolean = false,
|
||||||
external: Boolean = false,
|
external: Boolean = false,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue