Implemented replay

This commit is contained in:
kontiko 2024-06-27 22:22:13 +02:00
parent 6a3a37758c
commit 207b7f290f
12 changed files with 441 additions and 227 deletions

View file

@ -15,11 +15,8 @@
// Features to add to the dev container. More info: https://containers.dev/features. // Features to add to the dev container. More info: https://containers.dev/features.
// "features": {}, // "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally. // Use 'forwardPorts' to make a list of ports inside the container available locally.
//"forwardPorts": [ "forwardPorts": [3000, 3333],
// 3000, "runArgs": ["--network=host", "-p 3000:3000", "-p 3333:3333"],
// 3333
//],
"runArgs": ["--network=host"],
// Use 'postCreateCommand' to run commands after the container is created. // Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "sh .devcontainer/post-create.sh", "postCreateCommand": "sh .devcontainer/post-create.sh",
// Configure tool-specific properties. // Configure tool-specific properties.

26
.vscode/launch.json vendored
View file

@ -9,36 +9,24 @@
"request": "launch", "request": "launch",
"name": "Debug server", "name": "Debug server",
"runtimeExecutable": "npm", "runtimeExecutable": "npm",
"args": [ "args": ["run", "dev"],
"run", "skipFiles": ["<node_internals>/**"]
"dev"
],
"skipFiles": [
"<node_internals>/**"
]
}, },
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Debug client (nuxt)", "name": "Debug client (nuxt)",
"runtimeExecutable": "npm", "runtimeExecutable": "npm",
"args": [ "args": ["run", "dev"],
"run",
"dev"
],
"cwd": "${workspaceFolder}/client", "cwd": "${workspaceFolder}/client",
"skipFiles": [ "skipFiles": ["${workspaceFolder}/<node_internals>/**"]
"${workspaceFolder}/<node_internals>/**"
]
} }
], ],
"compounds": [ "compounds": [
{ {
"name": "Debug server and client (nuxt)", "name": "Debug server and client (nuxt)",
"configurations": [ "configurations": ["Debug server", "Debug client (nuxt)"]
"Debug server",
"Debug client (nuxt)"
]
} }
] ],
"outputCapture": "std"
} }

View file

@ -23,5 +23,6 @@
}, },
"[vue]": { "[vue]": {
"editor.defaultFormatter": "octref.vetur" "editor.defaultFormatter": "octref.vetur"
} },
"console": "integratedTerminal"
} }

View file

@ -11,7 +11,7 @@
<transition name="menu"> <transition name="menu">
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full min-w-48 bg-primary border border-black-200 shadow-lg rounded-b-md py-1 overflow-auto focus:outline-none sm:text-sm DlnaDropdownMenu" tabindex="-1" role="listbox"> <ul v-show="showMenu" class="absolute z-10 -mt-px w-full min-w-48 bg-primary border border-black-200 shadow-lg rounded-b-md py-1 overflow-auto focus:outline-none sm:text-sm DlnaDropdownMenu" tabindex="-1" role="listbox">
<template v-for="device in get_devices"> <template v-for="device in get_devices">
<li :key="device.host" class="text-gray-400 hover:text-white relative py-2 cursor-pointer hover:bg-black-400" v-bind:class="{ 'bg-black-600': get_device == device.host }" role="option" tabindex="0" @keydown.enter="selectDevice(device.host)" @click="selectDevice(device.host)"> <li :key="device.host" class="text-gray-400 hover:text-white relative py-2 cursor-pointer hover:bg-black-400" v-bind:class="{ 'bg-black-600': get_device == device.UDN }" role="option" tabindex="0" @keydown.enter="selectDevice(device.host)" @click="selectDevice(device.UDN)">
<div class="flex items-center px-2"> <div class="flex items-center px-2">
<span class="font-normal block truncate font-sans text-sm">{{ device.name }}</span> <span class="font-normal block truncate font-sans text-sm">{{ device.name }}</span>
</div> </div>

View file

@ -7,7 +7,7 @@ export default class DLNAPlayer extends EventEmitter {
this.ctx = ctx this.ctx = ctx
this.player = null this.player = null
this.playerController = null this.playerController = null
this.state = null
this.libraryItem = null this.libraryItem = null
this.audioTracks = [] this.audioTracks = []
this.currentTrackIndex = 0 this.currentTrackIndex = 0
@ -15,7 +15,8 @@ export default class DLNAPlayer extends EventEmitter {
this.currentTime = 0 this.currentTime = 0
this.playWhenReady = false this.playWhenReady = false
this.defaultPlaybackRate = 1 this.defaultPlaybackRate = 1
this.paused
this.currentTime = null
// TODO: Use canDisplayType on receiver to check mime types // TODO: Use canDisplayType on receiver to check mime types
this.playableMimeTypes = ['audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac', 'audio/x-ms-wma', 'audio/x-aiff', 'audio/webm'] this.playableMimeTypes = ['audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac', 'audio/x-ms-wma', 'audio/x-aiff', 'audio/webm']
@ -34,13 +35,15 @@ export default class DLNAPlayer extends EventEmitter {
} }
initialize() { initialize() {
this.ctx.$root.socket.on('test', this.destroy) this.ctx.$root.socket.on('dlna_status', (data) => this.setStatus(data))
//this.player = this.ctx.$root.castPlayer //this.player = this.ctx.$root.castPlayer
} }
evtMediaInfoChanged() {} evtMediaInfoChanged() {}
destroy() {} destroy() {
this.ctx.$root.socket.emit('dlna_exit')
}
async set(libraryItem, tracks, isHlsTranscode, startTime, playWhenReady = false) { async set(libraryItem, tracks, isHlsTranscode, startTime, playWhenReady = false) {
this.libraryItem = libraryItem this.libraryItem = libraryItem
@ -56,16 +59,28 @@ export default class DLNAPlayer extends EventEmitter {
} else { } else {
this.coverUrl = `${window.location.origin}${coverImg}` this.coverUrl = `${window.location.origin}${coverImg}`
} }
this.ctx.$root.socket.emit('dlna_start', this.audioTracks) var serverAddress = window.origin
if (this.$isDev) serverAddress = `http://localhost:3333${this.$config.routerBasePath}`
this.ctx.$root.socket.emit('dlna_start', this.ctx.$store.state.globals.dlnaDevice, this.audioTracks, startTime, serverAddress)
} }
resetStream(startTime) {} resetStream(startTime) {}
playPause() {} playPause() {
if (this.state == 'PLAYING') {
this.pause()
} else {
this.play()
}
}
play() {} play() {
this.ctx.$root.socket.emit('dlna_play')
}
pause() {} pause() {
this.ctx.$root.socket.emit('dlna_pause')
}
getCurrentTime() { getCurrentTime() {
//var currentTrackOffset = this.currentTrack.startOffset || 0 //var currentTrackOffset = this.currentTrack.startOffset || 0
@ -83,6 +98,22 @@ export default class DLNAPlayer extends EventEmitter {
} }
async seek(time, playWhenReady) {} async seek(time, playWhenReady) {}
setStatus(data) {
console.log(data)
if (this.state != data.status) {
this.state = data.status
this.emit('stateChange', this.state)
}
this.currentTrackIndex = data.track_idx
this.currentTime = data.pos
console.log(this.currentTrackIndex, this.currentTime)
}
getCurrentTime() {
return this.currentTime + this.audioTracks[this.currentTrackIndex].startOffset
}
seek(time) {
this.ctx.$root.socket.emit('dlna_seek', time)
}
setVolume(volume) {} setVolume(volume) {}
} }

View file

@ -13,7 +13,6 @@ const Logger = require('./Logger')
* @class Class for handling all the authentication related functionality. * @class Class for handling all the authentication related functionality.
*/ */
class Auth { class Auth {
constructor() { constructor() {
// Map of openId sessions indexed by oauth2 state-variable // Map of openId sessions indexed by oauth2 state-variable
this.openIdAuthSession = new Map() this.openIdAuthSession = new Map()
@ -24,40 +23,52 @@ class Auth {
*/ */
async initPassportJs() { async initPassportJs() {
// Check if we should load the local strategy (username + password login) // Check if we should load the local strategy (username + password login)
if (global.ServerSettings.authActiveAuthMethods.includes("local")) { if (global.ServerSettings.authActiveAuthMethods.includes('local')) {
this.initAuthStrategyPassword() this.initAuthStrategyPassword()
} }
// Check if we should load the openid strategy // Check if we should load the openid strategy
if (global.ServerSettings.authActiveAuthMethods.includes("openid")) { if (global.ServerSettings.authActiveAuthMethods.includes('openid')) {
this.initAuthStrategyOpenID() this.initAuthStrategyOpenID()
} }
// Load the JwtStrategy (always) -> for bearer token auth // Load the JwtStrategy (always) -> for bearer token auth
passport.use(new JwtStrategy({ passport.use(
new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), ExtractJwt.fromUrlQueryParameter('token')]), jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), ExtractJwt.fromUrlQueryParameter('token')]),
secretOrKey: Database.serverSettings.tokenSecret secretOrKey: Database.serverSettings.tokenSecret
}, this.jwtAuthCheck.bind(this))) },
this.jwtAuthCheck.bind(this)
)
)
// define how to seralize a user (to be put into the session) // define how to seralize a user (to be put into the session)
passport.serializeUser(function (user, cb) { passport.serializeUser(function (user, cb) {
process.nextTick(function () { process.nextTick(function () {
// only store id to session // only store id to session
return cb(null, JSON.stringify({ return cb(
id: user.id, null,
})) JSON.stringify({
id: user.id
})
)
}) })
}) })
// define how to deseralize a user (use the ID to get it from the database) // define how to deseralize a user (use the ID to get it from the database)
passport.deserializeUser((function (user, cb) { passport.deserializeUser(
process.nextTick((async function () { function (user, cb) {
process.nextTick(
async function () {
const parsedUserInfo = JSON.parse(user) const parsedUserInfo = JSON.parse(user)
// load the user by ID that is stored in the session // load the user by ID that is stored in the session
const dbUser = await Database.userModel.getUserById(parsedUserInfo.id) const dbUser = await Database.userModel.getUserById(parsedUserInfo.id)
return cb(null, dbUser) return cb(null, dbUser)
}).bind(this)) }.bind(this)
}).bind(this)) )
}.bind(this)
)
} }
/** /**
@ -92,13 +103,17 @@ class Auth {
client_secret: global.ServerSettings.authOpenIDClientSecret, client_secret: global.ServerSettings.authOpenIDClientSecret,
id_token_signed_response_alg: global.ServerSettings.authOpenIDTokenSigningAlgorithm id_token_signed_response_alg: global.ServerSettings.authOpenIDTokenSigningAlgorithm
}) })
passport.use('openid-client', new OpenIDClient.Strategy({ passport.use(
'openid-client',
new OpenIDClient.Strategy(
{
client: openIdClient, client: openIdClient,
params: { params: {
redirect_uri: '/auth/openid/callback', redirect_uri: '/auth/openid/callback',
scope: 'openid profile email' scope: 'openid profile email'
} }
}, async (tokenset, userinfo, done) => { },
async (tokenset, userinfo, done) => {
try { try {
Logger.debug(`[Auth] openid callback userinfo=`, JSON.stringify(userinfo, null, 2)) Logger.debug(`[Auth] openid callback userinfo=`, JSON.stringify(userinfo, null, 2))
@ -128,7 +143,9 @@ class Auth {
return done(null, null, 'Unauthorized') return done(null, null, 'Unauthorized')
} }
})) }
)
)
} }
/** /**
@ -181,7 +198,6 @@ class Auth {
return null return null
} }
user = await Database.userModel.getUserByUsername(username) user = await Database.userModel.getUserByUsername(username)
if (user?.authOpenIDSub) { if (user?.authOpenIDSub) {
@ -220,7 +236,8 @@ class Auth {
*/ */
validateGroupClaim(userinfo) { validateGroupClaim(userinfo) {
const groupClaimName = Database.serverSettings.authOpenIDGroupClaim const groupClaimName = Database.serverSettings.authOpenIDGroupClaim
if (!groupClaimName) // Allow no group claim when configured like this if (!groupClaimName)
// Allow no group claim when configured like this
return true return true
// If configured it must exist in userinfo // If configured it must exist in userinfo
@ -238,16 +255,16 @@ class Auth {
*/ */
async setUserGroup(user, userinfo) { async setUserGroup(user, userinfo) {
const groupClaimName = Database.serverSettings.authOpenIDGroupClaim const groupClaimName = Database.serverSettings.authOpenIDGroupClaim
if (!groupClaimName) // No group claim configured, don't set anything if (!groupClaimName)
// No group claim configured, don't set anything
return return
if (!userinfo[groupClaimName]) if (!userinfo[groupClaimName]) throw new Error(`Group claim ${groupClaimName} not found in userinfo`)
throw new Error(`Group claim ${groupClaimName} not found in userinfo`)
const groupsList = userinfo[groupClaimName].map(group => group.toLowerCase()) const groupsList = userinfo[groupClaimName].map((group) => group.toLowerCase())
const rolesInOrderOfPriority = ['admin', 'user', 'guest'] const rolesInOrderOfPriority = ['admin', 'user', 'guest']
let userType = rolesInOrderOfPriority.find(role => groupsList.includes(role)) let userType = rolesInOrderOfPriority.find((role) => groupsList.includes(role))
if (userType) { if (userType) {
if (user.type === 'root') { if (user.type === 'root') {
// Check OpenID Group // Check OpenID Group
@ -277,15 +294,14 @@ class Auth {
*/ */
async updateUserPermissions(user, userinfo) { async updateUserPermissions(user, userinfo) {
const absPermissionsClaim = Database.serverSettings.authOpenIDAdvancedPermsClaim const absPermissionsClaim = Database.serverSettings.authOpenIDAdvancedPermsClaim
if (!absPermissionsClaim) // No advanced permissions claim configured, don't set anything if (!absPermissionsClaim)
// No advanced permissions claim configured, don't set anything
return return
if (user.type === 'admin' || user.type === 'root') if (user.type === 'admin' || user.type === 'root') return
return
const absPermissions = userinfo[absPermissionsClaim] const absPermissions = userinfo[absPermissionsClaim]
if (!absPermissions) if (!absPermissions) throw new Error(`Advanced permissions claim ${absPermissionsClaim} not found in userinfo`)
throw new Error(`Advanced permissions claim ${absPermissionsClaim} not found in userinfo`)
if (user.updatePermissionsFromExternalJSON(absPermissions)) { if (user.updatePermissionsFromExternalJSON(absPermissions)) {
Logger.info(`[Auth] openid callback: Updating advanced perms for user "${user.username}" using "${JSON.stringify(absPermissions)}"`) Logger.info(`[Auth] openid callback: Updating advanced perms for user "${user.username}" using "${JSON.stringify(absPermissions)}"`)
@ -422,7 +438,7 @@ class Auth {
} }
// Generate a state on web flow or if no state supplied // Generate a state on web flow or if no state supplied
const state = (!isMobileFlow || !req.query.state) ? OpenIDClient.generators.random() : req.query.state const state = !isMobileFlow || !req.query.state ? OpenIDClient.generators.random() : req.query.state
// Redirect URL for the SSO provider // Redirect URL for the SSO provider
let redirectUri let redirectUri
@ -508,8 +524,7 @@ class Auth {
function isValidRedirectUri(uri) { function isValidRedirectUri(uri) {
// Check if the redirect_uri is in the whitelist // Check if the redirect_uri is in the whitelist
return Database.serverSettings.authOpenIDMobileRedirectURIs.includes(uri) || return Database.serverSettings.authOpenIDMobileRedirectURIs.includes(uri) || (Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*')
(Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*')
} }
}) })
@ -545,7 +560,9 @@ class Auth {
}) })
// openid strategy callback route (this receives the token from the configured openid login provider) // openid strategy callback route (this receives the token from the configured openid login provider)
router.get('/auth/openid/callback', (req, res, next) => { router.get(
'/auth/openid/callback',
(req, res, next) => {
const oidcStrategy = passport._strategy('openid-client') const oidcStrategy = passport._strategy('openid-client')
const sessionKey = oidcStrategy._key const sessionKey = oidcStrategy._key
@ -603,14 +620,14 @@ class Auth {
} }
} }
// While not required by the standard, the passport plugin re-sends the original redirect_uri in the token request // While not required by the standard, the passport plugin re-sends the original redirect_uri in the token request
// We need to set it correctly, as some SSO providers (e.g. keycloak) check that parameter when it is provided // We need to set it correctly, as some SSO providers (e.g. keycloak) check that parameter when it is provided
// We set it here again because the passport param can change between requests // We set it here again because the passport param can change between requests
return passport.authenticate('openid-client', { redirect_uri: req.session[sessionKey].sso_redirect_uri }, passportCallback(req, res, next))(req, res, next) return passport.authenticate('openid-client', { redirect_uri: req.session[sessionKey].sso_redirect_uri }, passportCallback(req, res, next))(req, res, next)
}, },
// on a successfull login: read the cookies and react like the client requested (callback or json) // on a successfull login: read the cookies and react like the client requested (callback or json)
this.handleLoginSuccessBasedOnCookie.bind(this)) this.handleLoginSuccessBasedOnCookie.bind(this)
)
/** /**
* Helper route used to auto-populate the openid URLs in config/authentication * Helper route used to auto-populate the openid URLs in config/authentication
@ -625,7 +642,7 @@ class Auth {
} }
if (!req.query.issuer) { if (!req.query.issuer) {
return res.status(400).send('Invalid request. Query param \'issuer\' is required') return res.status(400).send("Invalid request. Query param 'issuer' is required")
} }
// Strip trailing slash // Strip trailing slash
@ -641,10 +658,12 @@ class Auth {
} }
} catch (error) { } catch (error) {
Logger.error(`[Auth] Failed to get openid configuration. Invalid URL "${configUrl}"`, error) Logger.error(`[Auth] Failed to get openid configuration. Invalid URL "${configUrl}"`, error)
return res.status(400).send('Invalid request. Query param \'issuer\' is invalid') return res.status(400).send("Invalid request. Query param 'issuer' is invalid")
} }
axios.get(configUrl.toString()).then(({ data }) => { axios
.get(configUrl.toString())
.then(({ data }) => {
res.json({ res.json({
issuer: data.issuer, issuer: data.issuer,
authorization_endpoint: data.authorization_endpoint, authorization_endpoint: data.authorization_endpoint,
@ -654,7 +673,8 @@ class Auth {
jwks_uri: data.jwks_uri, jwks_uri: data.jwks_uri,
id_token_signing_alg_values_supported: data.id_token_signing_alg_values_supported id_token_signing_alg_values_supported: data.id_token_signing_alg_values_supported
}) })
}).catch((error) => { })
.catch((error) => {
Logger.error(`[Auth] Failed to get openid configuration at "${configUrl}"`, error) Logger.error(`[Auth] Failed to get openid configuration at "${configUrl}"`, error)
res.status(error.statusCode || 400).send(`${error.code || 'UNKNOWN'}: Failed to get openid configuration`) res.status(error.statusCode || 400).send(`${error.code || 'UNKNOWN'}: Failed to get openid configuration`)
}) })
@ -683,7 +703,7 @@ class Auth {
let postLogoutRedirectUri = null let postLogoutRedirectUri = null
if (authMethod === 'openid') { if (authMethod === 'openid') {
const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http' const protocol = req.secure || req.get('x-forwarded-proto') === 'https' ? 'https' : 'http'
const host = req.get('host') const host = req.get('host')
// TODO: ABS does currently not support subfolders for installation // TODO: ABS does currently not support subfolders for installation
// If we want to support it we need to include a config for the serverurl // If we want to support it we need to include a config for the serverurl
@ -727,7 +747,7 @@ class Auth {
next() next()
} else { } else {
// try JWT to authenticate // try JWT to authenticate
passport.authenticate("jwt")(req, res, next) passport.authenticate('jwt')(req, res, next)
} }
} }
@ -750,8 +770,7 @@ class Auth {
static validateAccessToken(token) { static validateAccessToken(token) {
try { try {
return jwt.verify(token, global.ServerSettings.tokenSecret) return jwt.verify(token, global.ServerSettings.tokenSecret)
} } catch (err) {
catch (err) {
return null return null
} }
} }
@ -760,7 +779,8 @@ class Auth {
* Generate a token which is used to encrpt/protect the jwts. * Generate a token which is used to encrpt/protect the jwts.
*/ */
async initTokenSecret() { async initTokenSecret() {
if (process.env.TOKEN_SECRET) { // User can supply their own token secret if (process.env.TOKEN_SECRET) {
// User can supply their own token secret
Database.serverSettings.tokenSecret = process.env.TOKEN_SECRET Database.serverSettings.tokenSecret = process.env.TOKEN_SECRET
} else { } else {
Database.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64') Database.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64')

View file

@ -78,7 +78,7 @@ class Server {
this.cronManager = new CronManager(this.podcastManager) this.cronManager = new CronManager(this.podcastManager)
this.apiCacheManager = new ApiCacheManager() this.apiCacheManager = new ApiCacheManager()
this.binaryManager = new BinaryManager() this.binaryManager = new BinaryManager()
this.DLNAManager = new DLNAManager() this.DLNAManager = new DLNAManager(this.auth)
// Routers // Routers
this.apiRouter = new ApiRouter(this) this.apiRouter = new ApiRouter(this)
@ -101,7 +101,7 @@ class Server {
} }
/** /**
* Initialize database, backups, logs, rss feeds, cron jobs & watcher * Initialize , backups, logs, rss feeds, cron jobs & watcher
* Cleanup stale/invalid data * Cleanup stale/invalid data
*/ */
async init() { async init() {

View file

@ -115,7 +115,11 @@ class SocketAuthority {
//DLNA //DLNA
socket.on('dlna_start', (data) => this.Server.DLNAManager.start_playback(socket.id, data)) socket.on('dlna_start', (player, audiobook, start_time, serverAddress) => this.Server.DLNAManager.start_playback(socket.id, player, audiobook, start_time, serverAddress))
socket.on('dlna_exit', () => this.Server.DLNAManager.exit_session(socket.id))
socket.on('dlna_play', () => this.Server.DLNAManager.continue_session(socket.id))
socket.on('dlna_pause', () => this.Server.DLNAManager.pause_session(socket.id))
socket.on('dlna_seek', (time) => this.Server.DLNAManager.seek(socket.id, time))
// Logs // Logs
socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level)) socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level))
socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id)) socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id))

View file

@ -1,10 +1,8 @@
class DLNAController { class DLNAController {
constructor() {} constructor() {}
async get_Devices(req, res) { async get_Devices(req, res) {
var count = 0 var count = 0
const players = await this.DLNAManager.Client.players const players = await this.DLNAManager.devices
return res.status(200).send(players) return res.status(200).send(players)
} }
middleware(req, res, next) { middleware(req, res, next) {

View file

@ -1,14 +1,80 @@
const { clientEmitter } = require('../SocketAuthority')
const DLNADevice = require('../objects/DLNADevice')
const DLNASession = require('../objects/DLNASession')
const SSDPclient = require('node-ssdp').Client
const Auth = require('../Auth')
class DLNAManager { class DLNAManager {
constructor() { constructor(auth) {
console.log('It worked') this.playback_sessions = []
this.Client = require('dlnacasts')() this.devices = []
this.Client.on('update', function (player) { this.ssdpclient = new SSDPclient()
console.log('all players: ', this.players) this.ssdpclient.on('response', this.add_device.bind(this))
}) setInterval(this.update_sessions.bind(this), 500)
this.ssdpclient.search('urn:schemas-upnp-org:service:AVTransport:1')
this.auth = auth
} }
add_device(header) {
start_playback(id, data) { console.log('header', header.LOCATION)
console.log(id, data) if (
!this.devices ||
!this.devices.find((dev) => {
return dev.url == header.LOCATION
})
) {
console.log('header', header.LOCATION)
this.devices.push(new DLNADevice(header.LOCATION))
console.log(this.devices)
}
}
start_playback(id, player, audiobook, start_time, serverAddress) {
var user = {
id: 42,
username: 'DLNA'
}
var token = this.auth.generateAccessToken(user)
this.playback_sessions.push(
new DLNASession(
id,
this.devices.find((pl) => {
return pl.UDN == player
}),
audiobook,
start_time,
serverAddress,
token
)
)
}
exit_session(id) {
//Stop playback when player is closed
console.log('Stop')
for (let item of this.playback_sessions) {
if (item.socket_id == id) {
item.player.pause()
}
}
this.playback_sessions = this.playback_sessions.filter((item) => !(item.socket_id == id))
console.log('sessions:', this.playback_sessions)
}
update_sessions() {
for (let pb_session of this.playback_sessions) {
pb_session.update()
}
}
pause_session(id) {
var session = this.playback_sessions.find((item) => item.socket_id == id)
console.log('bla', session)
session.player.pause()
}
continue_session(id) {
var session = this.playback_sessions.find((item) => item.socket_id == id)
console.log('playing', session)
session.player.play()
}
seek(id, time) {
console.log(id, time)
var session = this.playback_sessions.find((item) => item.socket_id == id)
session.load(time)
} }
} }

View file

@ -0,0 +1,19 @@
const MediaClient = require('upnp-mediarenderer-client')
class DLNADevice extends MediaClient {
constructor(url) {
console.log(url)
super(url)
this.name = ''
this.udn = ''
this.getDeviceDescription(this.update_Info.bind(this))
}
update_Info(err, desc) {
if (err) throw err
this.name = desc.friendlyName
this.UDN = desc.UDN
}
}
module.exports = DLNADevice

View file

@ -0,0 +1,90 @@
const SocketAuthority = require('../SocketAuthority')
const timespan = require('../libs/jsonwebtoken/lib/timespan')
const clientEmitter = require('../SocketAuthority')
class DLNASession {
constructor(id, player, audiobook, start_time, serverAddress, token) {
this.socket_id = id
this.player = player
this.tracks = audiobook
this.serverAddress = serverAddress
this.playtime = 0
this.is_active = true
this.token = token
this.status = 'PAUSED'
console.log(serverAddress)
this.trackIndex = null
this.player.pause()
this.load(start_time)
this.player.on('status', function (status) {
console.log('Session: ', status)
})
this.socket = SocketAuthority.clients[this.socket_id].socket
}
update() {
if (this.is_active) {
this.player.getTransportInfo(
function (err, info) {
var status = ''
if (info.CurrentTransportState == 'PLAYING') {
status = 'PLAYING'
} else if (info.CurrentTransportState == 'PAUSED_PLAYBACK') {
status = 'PAUSED'
} else if (info.CurrentTransportState == 'STOPPED') {
var t = this.tracks[this.trackIndex]
if (t.duration - this.playtime < 2) {
if (this.trackIndex < this.tracks.length) {
this.load(this.tracks[this.trackIndex + 1].startOffset)
this.playtime = 0
return
} else {
this.socket.emit('dlna_finished')
}
}
}
this.player.getPosition(
function (err, position) {
this.set_status(status, this.trackIndex, position)
console.log('pos:', position) // Current position in seconds
}.bind(this)
)
}.bind(this)
)
}
}
load(time) {
var idx = this.tracks.findIndex((t) => time >= t.startOffset && time < t.startOffset + t.duration)
var track = this.tracks[idx]
if (this.trackIndex != idx) {
this.trackIndex = idx
var options = {
autoplay: false,
contentType: track.mimeType,
metadata: {
type: 'audio' // can be 'video', 'audio' or 'image'
}
}
var url = this.serverAddress + track.contentUrl + '?token=' + track.userToken
console.log(this.tracks[this.trackIndex])
console.log(url)
this.player.load(
url,
options,
function (err, result) {
if (err) throw err
console.log('playing ...', this.playtime - track.startOffset)
this.player.play()
this.player.seek(time - track.startOffset)
}.bind(this)
)
} else {
this.player.seek(time - track.startOffset)
}
}
set_status(val, track, position) {
this.playtime = position
this.status = val
this.socket.emit('dlna_status', { status: val, track_idx: track, pos: position })
}
}
module.exports = DLNASession