Fixed sonos playback and add android background playback

This commit is contained in:
kontiko 2024-06-30 00:00:13 +02:00
parent 207b7f290f
commit f36df9e67b
13 changed files with 1814 additions and 1415 deletions

View file

@ -12,14 +12,14 @@ FROM node:20-alpine
ENV NODE_ENV=production
RUN apk update && \
apk add --no-cache --update \
curl \
tzdata \
ffmpeg \
make \
python3 \
g++ \
tini
apk add --no-cache --update \
curl \
tzdata \
ffmpeg \
make \
python3 \
g++ \
tini
COPY --from=tone /usr/local/bin/tone /usr/local/bin/
COPY --from=build /client/dist /client/dist
@ -30,7 +30,8 @@ RUN npm ci --only=production
RUN apk del make python3 g++
EXPOSE 80
ENV PORT=13377
EXPOSE 13377
ENTRYPOINT ["tini", "--"]
CMD ["node", "index.js"]

View file

@ -22,12 +22,7 @@ module.exports = {
htmlAttrs: {
lang: 'en'
},
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' },
{ hid: 'robots', name: 'robots', content: 'noindex' }
],
meta: [{ charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { hid: 'description', name: 'description', content: '' }, { hid: 'robots', name: 'robots', content: 'noindex' }],
script: [],
link: [
{ rel: 'icon', type: 'image/x-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/favicon.ico' },
@ -40,20 +35,10 @@ module.exports = {
},
// Global CSS: https://go.nuxtjs.dev/config-css
css: [
'@/assets/tailwind.css',
'@/assets/app.css'
],
css: ['@/assets/tailwind.css', '@/assets/app.css'],
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [
'@/plugins/constants.js',
'@/plugins/init.client.js',
'@/plugins/axios.js',
'@/plugins/toast.js',
'@/plugins/utils.js',
'@/plugins/i18n.js'
],
plugins: ['@/plugins/constants.js', '@/plugins/init.client.js', '@/plugins/axios.js', '@/plugins/toast.js', '@/plugins/utils.js', '@/plugins/i18n.js'],
// Auto import components: https://go.nuxtjs.dev/config-components
components: true,
@ -65,25 +50,24 @@ module.exports = {
],
// Modules: https://go.nuxtjs.dev/config-modules
modules: [
'nuxt-socket-io',
'@nuxtjs/axios',
'@nuxtjs/proxy'
],
modules: ['nuxt-socket-io', '@nuxtjs/axios', '@nuxtjs/proxy'],
proxy: {
'/api/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
'/public/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
'/dev/': { target: 'http://localhost:3333', pathRewrite: { '^/dev/': '' } }
},
io: {
sockets: [{
name: 'dev',
url: 'http://localhost:3333'
},
{
name: 'prod'
}]
sockets: [
{
name: 'dev',
url: 'http://localhost:3333'
},
{
name: 'prod'
}
]
},
// Axios module configuration: https://go.nuxtjs.dev/config-axios
@ -131,8 +115,8 @@ module.exports = {
postcss: {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
autoprefixer: {}
}
}
},
watchers: {
@ -147,12 +131,12 @@ module.exports = {
},
/**
* Temporary workaround for @nuxt-community/tailwindcss-module.
*
* Reported: 2022-05-23
* See: [Issue tracker](https://github.com/nuxt-community/tailwindcss-module/issues/480)
*/
* Temporary workaround for @nuxt-community/tailwindcss-module.
*
* Reported: 2022-05-23
* See: [Issue tracker](https://github.com/nuxt-community/tailwindcss-module/issues/480)
*/
devServerHandlers: [],
ignore: ["**/*.test.*", "**/*.cy.*"]
ignore: ['**/*.test.*', '**/*.cy.*']
}

View file

@ -17,6 +17,13 @@ export default class DLNAPlayer extends EventEmitter {
this.defaultPlaybackRate = 1
this.paused
this.currentTime = null
//This is stupid but the best way to keep the site loaded on Smartphones is to have a fake player playing in the background \(^_^)/
var audioEl = document.createElement('audio')
audioEl.id = 'audio-player'
audioEl.style.display = 'none'
audioEl.muted = true
document.body.appendChild(audioEl)
this.fake_player = audioEl
// 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']
@ -43,6 +50,9 @@ export default class DLNAPlayer extends EventEmitter {
destroy() {
this.ctx.$root.socket.emit('dlna_exit')
if (this.fake_player) {
this.fake_player.remove()
}
}
async set(libraryItem, tracks, isHlsTranscode, startTime, playWhenReady = false) {
@ -62,6 +72,9 @@ export default class DLNAPlayer extends EventEmitter {
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)
this.fake_player.src = tracks[0].relativeContentUrl
this.fake_player.load()
this.fake_player.play()
}
resetStream(startTime) {}
@ -100,10 +113,8 @@ export default class DLNAPlayer extends EventEmitter {
async seek(time, playWhenReady) {}
setStatus(data) {
console.log(data)
if (this.state != data.status) {
this.state = data.status
this.emit('stateChange', this.state)
}
this.state = data.status
this.emit('stateChange', this.state)
this.currentTrackIndex = data.track_idx
this.currentTime = data.pos
console.log(this.currentTrackIndex, this.currentTime)
@ -115,5 +126,8 @@ export default class DLNAPlayer extends EventEmitter {
seek(time) {
this.ctx.$root.socket.emit('dlna_seek', time)
}
setVolume(volume) {}
setVolume(volume) {
console.log('SetVolume')
this.ctx.$root.socket.emit('dlna_set_volume', volume)
}
}

View file

@ -3,11 +3,11 @@ version: "3.7"
services:
audiobookshelf:
image: ghcr.io/advplyr/audiobookshelf:latest
#image: ghcr.io/advplyr/audiobookshelf:latest
image: test
# ABS runs on port 13378 by default. If you want to change
# the port, only change the external port, not the internal port
ports:
- 13378:80
network_mode: "host"
volumes:
# These volumes are needed to keep your library persistent
# and allow media to be accessed by the ABS server.

3023
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -42,6 +42,7 @@
"graceful-fs": "^4.2.10",
"htmlparser2": "^8.0.1",
"lru-cache": "^10.0.3",
"node-ssdp": "^4.0.1",
"node-tone": "^1.0.1",
"nodemailer": "^6.9.13",
"openid-client": "^5.6.1",
@ -52,8 +53,8 @@
"socket.io": "^4.5.4",
"sqlite3": "^5.1.6",
"ssrf-req-filter": "^1.1.0",
"xml2js": "^0.5.0",
"dlnacasts": "^0.1.0"
"upnp-mediarenderer-client": "^1.4.0",
"xml2js": "^0.5.0"
},
"devDependencies": {
"chai": "^4.3.10",
@ -63,10 +64,8 @@
"sinon": "^17.0.1"
},
"overrides": {
"dlnacasts": {
"upnp-mediarenderer-client":{
"typescript": "git+https://github.com/kontiko/node-upnp-device-client-sonos-support.git"
}
"upnp-mediarenderer-client": {
"upnp-device-client": "git+https://github.com/kontiko/node-upnp-device-client-sonos-support.git"
}
}
}

View file

@ -83,7 +83,7 @@ class Server {
// Routers
this.apiRouter = new ApiRouter(this)
this.hlsRouter = new HlsRouter(this.auth, this.playbackSessionManager)
this.publicRouter = new PublicRouter()
this.publicRouter = new PublicRouter(this)
Logger.logManager = new LogManager()
@ -295,7 +295,8 @@ class Server {
'/config/item-metadata-utils/:id',
'/collection/:id',
'/playlist/:id',
'/share/:slug'
'/share/:slug',
'/dlna/:session/:id/track.*'
]
dyanimicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))

View file

@ -120,14 +120,17 @@ class SocketAuthority {
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))
socket.on('dlna_set_volume', (volume) => this.Server.DLNAManager.setVolume(socket.id, volume))
// Logs
socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level))
socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id))
// Sent automatically from socket.io clients
socket.on('disconnect', (reason) => {
//Stop playback when socket is closed
Logger.removeSocketListener(socket.id)
this.Server.DLNAManager.exit_session(socket.id)
const _client = this.clients[socket.id]
if (!_client) {
Logger.warn(`[SocketAuthority] Socket ${socket.id} disconnect, no client (Reason: ${reason})`)

View file

@ -8,5 +8,19 @@ class DLNAController {
middleware(req, res, next) {
next()
}
async get_file(req, res) {
console.log('id', req.params.id)
const session = await this.DLNAManager.playback_sessions.find((item) => item.socket_id == req.params.session)
if (!session) {
res.status(400).send('Session not found')
return
}
const track = session.tracks[req.params.id]
if (!track) {
res.status(400).send('Track not found')
return
}
return res.status(200).sendFile(track.metadata.path)
}
}
module.exports = new DLNAController()

View file

@ -14,24 +14,17 @@ class DLNAManager {
this.auth = auth
}
add_device(header) {
console.log('header', header.LOCATION)
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 = this.playback_sessions.filter((item) => !(item.socket_id == id))
this.playback_sessions.push(
new DLNASession(
id,
@ -40,14 +33,12 @@ class DLNAManager {
}),
audiobook,
start_time,
serverAddress,
token
serverAddress
)
)
}
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()
@ -63,18 +54,29 @@ class DLNAManager {
}
pause_session(id) {
var session = this.playback_sessions.find((item) => item.socket_id == id)
console.log('bla', session)
session.player.pause()
if (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()
if (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)
if (session) {
session.load(time)
}
}
setVolume(id, volume) {
console.log('SetVolume', volume)
var session = this.playback_sessions.find((item) => item.socket_id == id)
if (!session) {
return
}
session.player.setVolume(volume * 100)
}
}

View file

@ -2,7 +2,6 @@ const MediaClient = require('upnp-mediarenderer-client')
class DLNADevice extends MediaClient {
constructor(url) {
console.log(url)
super(url)
this.name = ''
this.udn = ''

View file

@ -1,23 +1,19 @@
const SocketAuthority = require('../SocketAuthority')
const timespan = require('../libs/jsonwebtoken/lib/timespan')
const clientEmitter = require('../SocketAuthority')
const Logger = require('../Logger')
class DLNASession {
constructor(id, player, audiobook, start_time, serverAddress, token) {
constructor(id, player, audiobook, start_time, serverAddress) {
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() {
@ -30,6 +26,7 @@ class DLNASession {
} else if (info.CurrentTransportState == 'PAUSED_PLAYBACK') {
status = 'PAUSED'
} else if (info.CurrentTransportState == 'STOPPED') {
//If playback is stopped check if playback of track ended and Load new track
var t = this.tracks[this.trackIndex]
if (t.duration - this.playtime < 2) {
if (this.trackIndex < this.tracks.length) {
@ -44,7 +41,6 @@ class DLNASession {
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)
@ -57,21 +53,21 @@ class DLNASession {
if (this.trackIndex != idx) {
this.trackIndex = idx
var options = {
autoplay: false,
autoplay: true,
contentType: track.mimeType,
metadata: {
type: 'audio' // can be 'video', 'audio' or 'image'
title: 'Test',
type: 'audio', // can be 'video', 'audio' or 'image'
subtitlesUrl: null
}
}
var url = this.serverAddress + track.contentUrl + '?token=' + track.userToken
console.log(this.tracks[this.trackIndex])
var url = `${this.serverAddress}/public/dlna/${this.socket_id}/${this.trackIndex}/track${track.metadata.ext}`
console.log(url)
this.player.load(
url,
options,
function (err, result) {
if (err) throw err
console.log('playing ...', this.playtime - track.startOffset)
if (err) Logger.error(err)
this.player.play()
this.player.seek(time - track.startOffset)
}.bind(this)

View file

@ -1,15 +1,18 @@
const express = require('express')
const ShareController = require('../controllers/ShareController')
const DLNAController = require('../controllers/DLNAController')
class PublicRouter {
constructor() {
constructor(Server) {
this.router = express()
this.router.disable('x-powered-by')
this.DLNAManager = Server.DLNAManager
this.init()
}
init() {
this.router.get('/share/:slug', ShareController.getMediaItemShareBySlug.bind(this))
this.router.get('/dlna/:session/:id/track.*', DLNAController.get_file.bind(this))
}
}
module.exports = PublicRouter