vlc/src/input/stream.c

931 lines
26 KiB
C

/*****************************************************************************
* stream.c
*****************************************************************************
* Copyright (C) 1999-2004 VLC authors and VideoLAN
* Copyright 2008-2015 Rémi Denis-Courmont
*
* Authors: Laurent Aimar <fenrir@via.ecp.fr>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include <assert.h>
#include <stdbool.h>
#include <stdalign.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <errno.h>
#include <vlc_common.h>
#include <vlc_block.h>
#include <vlc_access.h>
#include <vlc_charset.h>
#include <vlc_interrupt.h>
#include <vlc_stream_extractor.h>
#include <libvlc.h>
#include "stream.h"
#include "mrl_helpers.h"
typedef struct stream_priv_t
{
stream_t stream;
void (*destroy)(stream_t *);
block_t *block;
block_t *peek;
uint64_t offset;
bool eof;
/* UTF-16 and UTF-32 file reading */
struct {
vlc_iconv_t conv;
unsigned char char_width;
bool little_endian;
} text;
max_align_t private_data[];
} stream_priv_t;
/**
* Allocates a VLC stream object
*/
stream_t *vlc_stream_CustomNew(vlc_object_t *parent,
void (*destroy)(stream_t *), size_t size,
const char *type_name)
{
stream_priv_t *priv = vlc_custom_create(parent, sizeof (*priv) + size,
type_name);
if (unlikely(priv == NULL))
return NULL;
stream_t *s = &priv->stream;
s->psz_name = NULL;
s->psz_url = NULL;
s->psz_location = NULL;
s->psz_filepath = NULL;
s->b_preparsing = false;
s->p_input_item = NULL;
s->s = NULL;
s->out = NULL;
s->pf_read = NULL;
s->pf_block = NULL;
s->pf_readdir = NULL;
s->pf_demux = NULL;
s->pf_seek = NULL;
s->pf_control = NULL;
s->p_sys = NULL;
s->ops = NULL;
assert(destroy != NULL);
priv->destroy = destroy;
priv->block = NULL;
priv->peek = NULL;
priv->offset = 0;
priv->eof = false;
/* UTF16 and UTF32 text file conversion */
priv->text.conv = (vlc_iconv_t)(-1);
priv->text.char_width = 1;
priv->text.little_endian = false;
return &priv->stream;
}
#define stream_priv(s) container_of(s, stream_priv_t, stream)
void *vlc_stream_Private(stream_t *stream)
{
return stream_priv(stream)->private_data;
}
stream_t *vlc_stream_CommonNew(vlc_object_t *parent,
void (*destroy)(stream_t *))
{
return vlc_stream_CustomNew(parent, destroy, 0, "stream");
}
void stream_CommonDelete(stream_t *s)
{
stream_priv_t *priv = stream_priv(s);
if (priv->text.conv != (vlc_iconv_t)(-1))
vlc_iconv_close(priv->text.conv);
if (priv->peek != NULL)
block_Release(priv->peek);
if (priv->block != NULL)
block_Release(priv->block);
free(s->psz_url);
vlc_object_delete(s);
}
/**
* Destroy a stream
*/
void vlc_stream_Delete(stream_t *s)
{
stream_priv_t *priv = stream_priv(s);
if (s->ops != NULL && s->ops->close != NULL) {
s->ops->close(s);
}
priv->destroy(s);
stream_CommonDelete(s);
}
stream_t *(vlc_stream_NewURL)(vlc_object_t *p_parent, const char *psz_url)
{
if( !psz_url )
return NULL;
stream_t *s = stream_AccessNew( p_parent, NULL, NULL, false, psz_url );
if( s == NULL )
msg_Err( p_parent, "no suitable access module for `%s'", psz_url );
else
s = stream_FilterAutoNew(s);
return s;
}
stream_t *(vlc_stream_NewMRL)(vlc_object_t* parent, const char* mrl )
{
stream_t* stream = vlc_stream_NewURL( parent, mrl );
if( stream == NULL )
return NULL;
char const* anchor = strchr( mrl, '#' );
if( anchor == NULL )
return stream;
char const* extra;
if( stream_extractor_AttachParsed( &stream, anchor + 1, &extra ) )
{
msg_Err( parent, "unable to open %s", mrl );
vlc_stream_Delete( stream );
return NULL;
}
if( extra && *extra )
msg_Warn( parent, "ignoring extra fragment data: %s", extra );
return stream;
}
/**
* Read from the stream until first newline.
* \param s Stream handle to read from
* \return A pointer to the allocated output string. You need to free this when you are done.
*/
#define STREAM_PROBE_LINE 2048
#define STREAM_LINE_MAX (2048*100)
char *vlc_stream_ReadLine( stream_t *s )
{
stream_priv_t *priv = stream_priv(s);
/* Let's fail quickly if this is a readdir access */
if( (s->ops != NULL && s->ops->stream.read == NULL && s->ops->stream.block == NULL) ||
(s->ops == NULL && s->pf_read == NULL && s->pf_block == NULL) )
return NULL;
/* BOM detection */
if( vlc_stream_Tell( s ) == 0 )
{
const uint8_t *p_data;
ssize_t i_data = vlc_stream_Peek( s, &p_data, 2 );
if( i_data <= 0 )
return NULL;
if( unlikely(priv->text.conv != (vlc_iconv_t)-1) )
{ /* seek back to beginning? reset */
vlc_iconv_close( priv->text.conv );
priv->text.conv = (vlc_iconv_t)-1;
}
priv->text.char_width = 1;
priv->text.little_endian = false;
if( i_data >= 2 )
{
const char *psz_encoding = NULL;
bool little_endian = false;
if( !memcmp( p_data, "\xFF\xFE", 2 ) )
{
psz_encoding = "UTF-16LE";
little_endian = true;
}
else if( !memcmp( p_data, "\xFE\xFF", 2 ) )
{
psz_encoding = "UTF-16BE";
}
/* Open the converter if we need it */
if( psz_encoding != NULL )
{
msg_Dbg( s, "UTF-16 BOM detected" );
priv->text.conv = vlc_iconv_open( "UTF-8", psz_encoding );
if( unlikely(priv->text.conv == (vlc_iconv_t)-1) )
{
msg_Err( s, "iconv_open failed" );
return NULL;
}
priv->text.char_width = 2;
priv->text.little_endian = little_endian;
}
}
}
size_t i_line = 0;
const uint8_t *p_data;
for( ;; )
{
size_t i_peek = i_line == 0 ? STREAM_PROBE_LINE
: __MIN( i_line * 2, STREAM_LINE_MAX );
/* Probe more data */
ssize_t i_data = vlc_stream_Peek( s, &p_data, i_peek );
if( i_data <= 0 )
return NULL;
/* Deal here with lone-byte incomplete UTF-16 sequences at EOF
that we won't be able to process anyway */
if( i_data < priv->text.char_width )
{
assert( priv->text.char_width == 2 );
uint8_t inc;
ssize_t i_inc = vlc_stream_Read( s, &inc, priv->text.char_width );
assert( i_inc == i_data );
if( i_inc > 0 )
msg_Err( s, "discarding incomplete UTF-16 sequence at EOF: 0x%02x", inc );
return NULL;
}
/* Keep to text encoding character width boundary */
if( i_data % priv->text.char_width )
i_data = i_data - ( i_data % priv->text.char_width );
if( (size_t) i_data == i_line )
break; /* No more data */
assert( (size_t) i_data > i_line );
/* Resume search for an EOL where we left off */
const uint8_t *p_cur = p_data + i_line, *psz_eol;
/* FIXME: <CR> behavior varies depending on where buffer
boundaries happen to fall; a <CR><LF> across the boundary
creates a bogus empty line. */
if( priv->text.char_width == 1 )
{
/* UTF-8: 0A <LF> */
psz_eol = memchr( p_cur, '\n', i_data - i_line );
if( psz_eol == NULL )
/* UTF-8: 0D <CR> */
psz_eol = memchr( p_cur, '\r', i_data - i_line );
}
else
{
const uint8_t *p_last = p_data + i_data - priv->text.char_width;
uint16_t eol = priv->text.little_endian ? 0x0A00 : 0x000A;
assert( priv->text.char_width == 2 );
psz_eol = NULL;
/* UTF-16: 000A <LF> */
for( const uint8_t *p = p_cur; p <= p_last; p += 2 )
{
if( U16_AT( p ) == eol )
{
psz_eol = p;
break;
}
}
if( psz_eol == NULL )
{ /* UTF-16: 000D <CR> */
eol = priv->text.little_endian ? 0x0D00 : 0x000D;
for( const uint8_t *p = p_cur; p <= p_last; p += 2 )
{
if( U16_AT( p ) == eol )
{
psz_eol = p;
break;
}
}
}
}
if( psz_eol )
{
i_line = (psz_eol - p_data) + priv->text.char_width;
/* We have our line */
break;
}
i_line = i_data;
if( i_line >= STREAM_LINE_MAX )
{
msg_Err( s, "line too long, exceeding %zu bytes",
(size_t) STREAM_LINE_MAX );
return NULL;
}
}
if( i_line == 0 ) /* We failed to read any data, probably EOF */
return NULL;
/* If encoding conversion is required, UTF-8 needs at most 150%
as long a buffer as UTF-16 */
size_t i_line_conv = priv->text.char_width == 1 ? i_line : i_line * 3 / 2;
char *p_line = malloc( i_line_conv + 1 ); /* +1 for easy \0 append */
if( !p_line )
return NULL;
void *p_read = p_line;
if( priv->text.char_width > 1 )
{
size_t i_in = i_line, i_out = i_line_conv;
const char * p_in = (char *) p_data;
char * p_out = p_line;
if( vlc_iconv( priv->text.conv, &p_in, &i_in, &p_out, &i_out ) == VLC_ICONV_ERR )
{
msg_Err( s, "conversion error: %s", vlc_strerror_c( errno ) );
msg_Dbg( s, "original: %zu, in %zu, out %zu", i_line, i_in, i_out );
/* Reset state */
size_t r = vlc_iconv( priv->text.conv, NULL, NULL, NULL, NULL );
VLC_UNUSED( r );
/* FIXME: the rest of the line is discarded and lost */
}
i_line_conv -= i_out;
p_read = NULL; /* Line already read, only need to advance the stream */
}
ssize_t i_data = vlc_stream_Read( s, p_read, i_line );
assert( i_data > 0 && (size_t) i_data == i_line );
if( i_data <= 0 )
{
/* Hmmm */
free( p_line );
return NULL;
}
i_line = i_line_conv;
/* Remove trailing LF/CR */
while( i_line >= 1 &&
(p_line[i_line - 1] == '\r' || p_line[i_line - 1] == '\n') )
i_line--;
/* Make sure the \0 is there */
p_line[i_line] = '\0';
return p_line;
}
static ssize_t vlc_stream_CopyBlock(block_t **restrict pp,
void *buf, size_t len)
{
block_t *block = *pp;
if (block == NULL)
return -1;
if (len > block->i_buffer)
len = block->i_buffer;
if (buf != NULL)
memcpy(buf, block->p_buffer, len);
block->p_buffer += len;
block->i_buffer -= len;
if (block->i_buffer == 0)
{
block_Release(block);
*pp = NULL;
}
return likely(len > 0) ? (ssize_t)len : -1;
}
static ssize_t vlc_stream_ReadRaw(stream_t *s, void *buf, size_t len)
{
stream_priv_t *priv = stream_priv(s);
ssize_t ret;
assert(len <= SSIZE_MAX);
if (vlc_killed())
return 0;
if ((s->ops != NULL && s->ops->stream.read != NULL) || (s->ops == NULL && s->pf_read != NULL))
{
assert(priv->block == NULL);
if (buf == NULL)
{
if (unlikely(len == 0))
return 0;
char dummy[256];
ret = (s->ops != NULL ? s->ops->stream.read : s->pf_read)(s, dummy, len <= 256 ? len : 256);
}
else
ret = (s->ops != NULL ? s->ops->stream.read : s->pf_read)(s, buf, len);
return ret;
}
ret = vlc_stream_CopyBlock(&priv->block, buf, len);
if (ret >= 0)
return ret;
if ((s->ops != NULL && s->ops->stream.block != NULL) || (s->ops == NULL && s->pf_block != NULL))
{
bool eof = false;
priv->block = (s->ops != NULL ? s->ops->stream.block : s->pf_block)(s, &eof);
ret = vlc_stream_CopyBlock(&priv->block, buf, len);
if (ret >= 0)
return ret;
return eof ? 0 : -1;
}
return 0;
}
ssize_t vlc_stream_ReadPartial(stream_t *s, void *buf, size_t len)
{
stream_priv_t *priv = stream_priv(s);
ssize_t ret;
ret = vlc_stream_CopyBlock(&priv->peek, buf, len);
if (ret >= 0)
{
priv->offset += ret;
assert(ret <= (ssize_t)len);
return ret;
}
ret = vlc_stream_ReadRaw(s, buf, len);
if (ret > 0)
priv->offset += ret;
if (ret == 0)
priv->eof = len != 0;
assert(ret <= (ssize_t)len);
return ret;
}
ssize_t vlc_stream_Read(stream_t *s, void *buf, size_t len)
{
size_t copied = 0;
while (len > 0)
{
ssize_t ret = vlc_stream_ReadPartial(s, buf, len);
if (ret < 0)
continue;
if (ret == 0)
break;
if (buf != NULL)
buf = (char *)buf + ret;
assert(len >= (size_t)ret);
len -= ret;
copied += ret;
}
return copied;
}
ssize_t vlc_stream_Peek(stream_t *s, const uint8_t **restrict bufp, size_t len)
{
stream_priv_t *priv = stream_priv(s);
block_t *peek;
peek = priv->peek;
if (peek == NULL)
{
peek = priv->block;
priv->peek = peek;
priv->block = NULL;
}
if (peek == NULL)
{
peek = block_Alloc(len);
if (unlikely(peek == NULL))
return VLC_ENOMEM;
peek->i_buffer = 0;
}
else
if (peek->i_buffer < len)
{
size_t avail = peek->i_buffer;
peek = block_TryRealloc(peek, 0, len);
if (unlikely(peek == NULL))
return VLC_ENOMEM;
peek->i_buffer = avail;
}
priv->peek = peek;
*bufp = peek->p_buffer;
while (peek->i_buffer < len)
{
size_t avail = peek->i_buffer;
ssize_t ret;
ret = vlc_stream_ReadRaw(s, peek->p_buffer + avail, len - avail);
if (ret < 0)
continue;
peek->i_buffer += ret;
if (ret == 0)
return peek->i_buffer;
}
return len;
}
block_t *vlc_stream_ReadBlock(stream_t *s)
{
stream_priv_t *priv = stream_priv(s);
block_t *block;
if (vlc_killed())
{
priv->eof = true;
return NULL;
}
if (priv->peek != NULL)
{
block = priv->peek;
priv->peek = NULL;
}
else if (priv->block != NULL)
{
block = priv->block;
priv->block = NULL;
}
else if ((s->ops != NULL && s->ops->stream.block != NULL) || (s->ops == NULL && s->pf_block != NULL))
{
priv->eof = false;
block = (s->ops != NULL ? s->ops->stream.block : s->pf_block)(s, &priv->eof);
}
else
{
block = block_Alloc(4096);
if (unlikely(block == NULL))
return NULL;
ssize_t ret = (s->ops != NULL ? s->ops->stream.read : s->pf_read)(s, block->p_buffer, block->i_buffer);
if (ret > 0)
block->i_buffer = ret;
else
{
block_Release(block);
block = NULL;
}
priv->eof = !ret;
}
if (block != NULL)
priv->offset += block->i_buffer;
return block;
}
uint64_t vlc_stream_Tell(const stream_t *s)
{
const stream_priv_t *priv = stream_priv(s);
return priv->offset;
}
bool vlc_stream_Eof(const stream_t *s)
{
const stream_priv_t *priv = stream_priv(s);
return priv->eof;
}
int vlc_stream_Seek(stream_t *s, uint64_t offset)
{
stream_priv_t *priv = stream_priv(s);
priv->eof = false;
block_t *peek = priv->peek;
if (peek != NULL)
{
if (offset >= priv->offset
&& offset <= (priv->offset + peek->i_buffer))
{ /* Seeking within the peek buffer */
size_t fwd = offset - priv->offset;
peek->p_buffer += fwd;
peek->i_buffer -= fwd;
priv->offset = offset;
if (peek->i_buffer == 0)
{
priv->peek = NULL;
block_Release(peek);
}
return VLC_SUCCESS;
}
}
else
{
if (priv->offset == offset)
return VLC_SUCCESS; /* Nothing to do! */
}
int ret;
if (s->ops == NULL && s->pf_seek != NULL) {
ret = s->pf_seek(s, offset);
} else if (s->ops != NULL && s->ops->stream.seek != NULL) {
ret = s->ops->stream.seek(s, offset);
} else {
return VLC_EGENERIC;
}
if (ret != VLC_SUCCESS)
return ret;
priv->offset = offset;
if (peek != NULL)
{
priv->peek = NULL;
block_Release(peek);
}
if (priv->block != NULL)
{
block_Release(priv->block);
priv->block = NULL;
}
return VLC_SUCCESS;
}
/**
* Use to control the "stream_t *". Look at #stream_query_e for
* possible "i_query" value and format arguments. Return VLC_SUCCESS
* if ... succeed ;) and VLC_EGENERIC if failed or unimplemented
*/
int vlc_stream_vaControl(stream_t *s, int cmd, va_list args)
{
stream_priv_t *priv = stream_priv(s);
switch (cmd)
{
case STREAM_SET_TITLE:
case STREAM_SET_SEEKPOINT:
{
int ret;
if (s->ops != NULL && cmd == STREAM_SET_TITLE) {
int title = va_arg(args, int);
ret = s->ops->set_title(s, title);
} else if (s->ops != NULL && cmd == STREAM_SET_SEEKPOINT) {
int seekpoint = va_arg(args, int);
ret = s->ops->set_seek_point(s, seekpoint);
} else {
ret = s->pf_control(s, cmd, args);
}
if (ret != VLC_SUCCESS)
return ret;
priv->offset = 0;
if (priv->peek != NULL)
{
block_Release(priv->peek);
priv->peek = NULL;
}
if (priv->block != NULL)
{
block_Release(priv->block);
priv->block = NULL;
}
return VLC_SUCCESS;
}
}
if (s->ops == NULL)
return s->pf_control(s, cmd, args);
switch (cmd) {
case STREAM_CAN_SEEK:
{
bool *can_seek = va_arg(args, bool *);
if (s->ops->can_seek != NULL) {
*can_seek = s->ops->can_seek(s);
} else {
*can_seek = false;
}
return VLC_SUCCESS;
}
case STREAM_CAN_FASTSEEK:
{
bool *can_fastseek = va_arg(args, bool *);
if (s->ops->stream.can_fastseek != NULL) {
*can_fastseek = s->ops->stream.can_fastseek(s);
} else {
*can_fastseek = false;
}
return VLC_SUCCESS;
}
case STREAM_CAN_PAUSE:
{
bool *can_pause = va_arg(args, bool *);
if (s->ops->can_pause != NULL) {
*can_pause = s->ops->can_pause(s);
} else {
*can_pause = false;
}
return VLC_SUCCESS;
}
case STREAM_CAN_CONTROL_PACE:
{
bool *can_control_pace = va_arg(args, bool *);
if (s->ops->can_control_pace != NULL) {
*can_control_pace = s->ops->can_control_pace(s);
} else {
*can_control_pace = false;
}
return VLC_SUCCESS;
}
case STREAM_GET_SIZE:
if (s->ops->stream.get_size != NULL) {
uint64_t *size = va_arg(args, uint64_t *);
return s->ops->stream.get_size(s, size);
}
return VLC_EGENERIC;
case STREAM_GET_PTS_DELAY:
if (s->ops->get_pts_delay != NULL) {
vlc_tick_t *pts_delay = va_arg(args, vlc_tick_t *);
return s->ops->get_pts_delay(s, pts_delay);
}
return VLC_EGENERIC;
case STREAM_GET_TITLE_INFO:
if (s->ops->stream.get_title_info != NULL) {
input_title_t ***title_info = va_arg(args, input_title_t ***);
int *unk = va_arg(args, int *);
return s->ops->stream.get_title_info(s, title_info, unk);
}
return VLC_EGENERIC;
case STREAM_GET_TITLE:
if (s->ops->stream.get_title != NULL) {
unsigned *title = va_arg(args, unsigned *);
return s->ops->stream.get_title(s, title);
}
return VLC_EGENERIC;
case STREAM_GET_SEEKPOINT:
if (s->ops->stream.get_seekpoint != NULL) {
unsigned *seekpoint = va_arg(args, unsigned *);
return s->ops->stream.get_seekpoint(s, seekpoint);
}
return VLC_EGENERIC;
case STREAM_GET_META:
if (s->ops->get_meta != NULL) {
vlc_meta_t *meta = va_arg(args, vlc_meta_t *);
return s->ops->get_meta(s, meta);
}
return VLC_EGENERIC;
case STREAM_GET_CONTENT_TYPE:
if (s->ops->stream.get_content_type != NULL) {
char **content_type = va_arg(args, char **);
return s->ops->stream.get_content_type(s, content_type);
}
return VLC_EGENERIC;
case STREAM_GET_SIGNAL:
if (s->ops->get_signal != NULL) {
double *quality = va_arg(args, double *);
double *strength = va_arg(args, double *);
return s->ops->get_signal(s, quality, strength);
}
return VLC_EGENERIC;
case STREAM_GET_TAGS:
if (s->ops->stream.get_tags != NULL) {
const block_t **block = va_arg(args, const block_t **);
return s->ops->stream.get_tags(s, block);
}
return VLC_EGENERIC;
case STREAM_GET_TYPE:
if (s->ops->get_type != NULL) {
int *type = va_arg(args, int *);
return s->ops->get_type(s, type);
}
return VLC_EGENERIC;
case STREAM_GET_PRIVATE_ID_STATE:
if (s->ops->stream.get_private_id_state != NULL) {
int priv_data = va_arg(args, int);
bool *selected = va_arg(args, bool *);
return s->ops->stream.get_private_id_state(s, priv_data, selected);
}
return VLC_EGENERIC;
case STREAM_SET_PAUSE_STATE:
if (s->ops->set_pause_state != NULL) {
bool pause_state = (bool)va_arg(args, int);
return s->ops->set_pause_state(s, pause_state);
}
return VLC_EGENERIC;
case STREAM_SET_RECORD_STATE:
if (s->ops->stream.set_record_state != NULL) {
bool record_state = (bool)va_arg(args, int);
const char *dir_path = NULL;
const char *ext = NULL;
if (record_state) {
dir_path = va_arg(args, const char *);
ext = va_arg(args, const char *);
}
return s->ops->stream.set_record_state(s, record_state, dir_path, ext);
}
return VLC_EGENERIC;
case STREAM_SET_PRIVATE_ID_STATE:
if (s->ops->stream.set_private_id_state != NULL) {
int priv_data = va_arg(args, int);
bool selected = (bool)va_arg(args, int);
return s->ops->stream.set_private_id_state(s, priv_data, selected);
}
return VLC_EGENERIC;
case STREAM_SET_PRIVATE_ID_CA:
if (s->ops->stream.set_private_id_ca != NULL) {
void *payload = va_arg(args, void *);
return s->ops->stream.set_private_id_ca(s, payload);
}
return VLC_EGENERIC;
default:
vlc_assert_unreachable();
}
}
/**
* Read data into a block.
*
* @param s stream to read data from
* @param size number of bytes to read
* @return a block of data, or NULL on error
@ note The block size may be shorter than requested if the end-of-stream was
* reached.
*/
block_t *vlc_stream_Block( stream_t *s, size_t size )
{
if( unlikely(size > SSIZE_MAX) )
return NULL;
block_t *block = block_Alloc( size );
if( unlikely(block == NULL) )
return NULL;
ssize_t val = vlc_stream_Read( s, block->p_buffer, size );
if( val <= 0 )
{
block_Release( block );
return NULL;
}
block->i_buffer = val;
return block;
}
int vlc_stream_ReadDir( stream_t *s, input_item_node_t *p_node )
{
assert(s->pf_readdir != NULL || (s->ops != NULL && s->ops->stream.readdir != NULL));
return (s->ops != NULL ? s->ops->stream.readdir : s->pf_readdir)( s, p_node );
}