/*
* Live HDS fragmenter
* Copyright ( c ) 2013 Martin Storsjo
*
* This file is part of FFmpeg .
*
* FFmpeg 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 .
*
* FFmpeg 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 FFmpeg ; if not , write to the Free Software
* Foundation , Inc . , 51 Franklin Street , Fifth Floor , Boston , MA 02110 - 1301 USA
*/
# include "config.h"
# include <float.h>
# if HAVE_UNISTD_H
# include <unistd.h>
# endif
# include "avformat.h"
# include "internal.h"
# include "os_support.h"
# include "libavutil/avstring.h"
# include "libavutil/base64.h"
# include "libavutil/intreadwrite.h"
# include "libavutil/mathematics.h"
# include "libavutil/opt.h"
typedef struct Fragment {
char file [ 1024 ] ;
int64_t start_time , duration ;
int n ;
} Fragment ;
typedef struct OutputStream {
int bitrate ;
int first_stream ;
AVFormatContext * ctx ;
int ctx_inited ;
uint8_t iobuf [ 32768 ] ;
char temp_filename [ 1024 ] ;
int64_t frag_start_ts , last_ts ;
AVIOContext * out ;
int packets_written ;
int nb_fragments , fragments_size , fragment_index ;
Fragment * * fragments ;
int has_audio , has_video ;
uint8_t * metadata ;
int metadata_size ;
uint8_t * extra_packets [ 2 ] ;
int extra_packet_sizes [ 2 ] ;
int nb_extra_packets ;
} OutputStream ;
typedef struct HDSContext {
const AVClass * class ; /* Class for private options. */
int window_size ;
int extra_window_size ;
int min_frag_duration ;
int remove_at_exit ;
OutputStream * streams ;
int nb_streams ;
} HDSContext ;
static int parse_header ( OutputStream * os , const uint8_t * buf , int buf_size )
{
if ( buf_size < 13 )
return AVERROR_INVALIDDATA ;
if ( memcmp ( buf , " FLV " , 3 ) )
return AVERROR_INVALIDDATA ;
buf + = 13 ;
buf_size - = 13 ;
while ( buf_size > = 11 + 4 ) {
int type = buf [ 0 ] ;
int size = AV_RB24 ( & buf [ 1 ] ) + 11 + 4 ;
if ( size > buf_size )
return AVERROR_INVALIDDATA ;
if ( type = = 8 | | type = = 9 ) {
if ( os - > nb_extra_packets > = FF_ARRAY_ELEMS ( os - > extra_packets ) )
return AVERROR_INVALIDDATA ;
os - > extra_packet_sizes [ os - > nb_extra_packets ] = size ;
os - > extra_packets [ os - > nb_extra_packets ] = av_malloc ( size ) ;
if ( ! os - > extra_packets [ os - > nb_extra_packets ] )
return AVERROR ( ENOMEM ) ;
memcpy ( os - > extra_packets [ os - > nb_extra_packets ] , buf , size ) ;
os - > nb_extra_packets + + ;
} else if ( type = = 0x12 ) {
if ( os - > metadata )
return AVERROR_INVALIDDATA ;
os - > metadata_size = size - 11 - 4 ;
os - > metadata = av_malloc ( os - > metadata_size ) ;
if ( ! os - > metadata )
return AVERROR ( ENOMEM ) ;
memcpy ( os - > metadata , buf + 11 , os - > metadata_size ) ;
}
buf + = size ;
buf_size - = size ;
}
if ( ! os - > metadata )
return AVERROR_INVALIDDATA ;
return 0 ;
}
static int hds_write ( void * opaque , uint8_t * buf , int buf_size )
{
OutputStream * os = opaque ;
if ( os - > out ) {
avio_write ( os - > out , buf , buf_size ) ;
} else {
if ( ! os - > metadata_size ) {
int ret ;
// Assuming the IO buffer is large enough to fit the
// FLV header and all metadata and extradata packets
if ( ( ret = parse_header ( os , buf , buf_size ) ) < 0 )
return ret ;
}
}
return buf_size ;
}
static void hds_free ( AVFormatContext * s )
{
HDSContext * c = s - > priv_data ;
int i , j ;
if ( ! c - > streams )
return ;
for ( i = 0 ; i < s - > nb_streams ; i + + ) {
OutputStream * os = & c - > streams [ i ] ;
avio_closep ( & os - > out ) ;
if ( os - > ctx & & os - > ctx_inited )
av_write_trailer ( os - > ctx ) ;
if ( os - > ctx )
av_freep ( & os - > ctx - > pb ) ;
if ( os - > ctx )
avformat_free_context ( os - > ctx ) ;
av_freep ( & os - > metadata ) ;
for ( j = 0 ; j < os - > nb_extra_packets ; j + + )
av_freep ( & os - > extra_packets [ j ] ) ;
for ( j = 0 ; j < os - > nb_fragments ; j + + )
av_freep ( & os - > fragments [ j ] ) ;
av_freep ( & os - > fragments ) ;
}
av_freep ( & c - > streams ) ;
}
static int write_manifest ( AVFormatContext * s , int final )
{
HDSContext * c = s - > priv_data ;
AVIOContext * out ;
char filename [ 1024 ] , temp_filename [ 1024 ] ;
int ret , i ;
float duration = 0 ;
if ( c - > nb_streams > 0 )
duration = c - > streams [ 0 ] . last_ts * av_q2d ( s - > streams [ 0 ] - > time_base ) ;
snprintf ( filename , sizeof ( filename ) , " %s/index.f4m " , s - > filename ) ;
snprintf ( temp_filename , sizeof ( temp_filename ) , " %s/index.f4m.tmp " , s - > filename ) ;
ret = avio_open2 ( & out , temp_filename , AVIO_FLAG_WRITE ,
& s - > interrupt_callback , NULL ) ;
if ( ret < 0 ) {
av_log ( s , AV_LOG_ERROR , " Unable to open %s for writing \n " , temp_filename ) ;
return ret ;
}
avio_printf ( out , " <?xml version= \" 1.0 \" encoding= \" utf-8 \" ?> \n " ) ;
avio_printf ( out , " <manifest xmlns= \" http://ns.adobe.com/f4m/1.0 \" > \n " ) ;
avio_printf ( out , " \t <id>%s</id> \n " , av_basename ( s - > filename ) ) ;
avio_printf ( out , " \t <streamType>%s</streamType> \n " ,
final ? " recorded " : " live " ) ;
avio_printf ( out , " \t <deliveryType>streaming</deliveryType> \n " ) ;
if ( final )
avio_printf ( out , " \t <duration>%f</duration> \n " , duration ) ;
for ( i = 0 ; i < c - > nb_streams ; i + + ) {
OutputStream * os = & c - > streams [ i ] ;
int b64_size = AV_BASE64_SIZE ( os - > metadata_size ) ;
char * base64 = av_malloc ( b64_size ) ;
if ( ! base64 ) {
avio_close ( out ) ;
return AVERROR ( ENOMEM ) ;
}
av_base64_encode ( base64 , b64_size , os - > metadata , os - > metadata_size ) ;
avio_printf ( out , " \t <bootstrapInfo profile= \" named \" url= \" stream%d.abst \" id= \" bootstrap%d \" /> \n " , i , i ) ;
avio_printf ( out , " \t <media bitrate= \" %d \" url= \" stream%d \" bootstrapInfoId= \" bootstrap%d \" > \n " , os - > bitrate / 1000 , i , i ) ;
avio_printf ( out , " \t \t <metadata>%s</metadata> \n " , base64 ) ;
avio_printf ( out , " \t </media> \n " ) ;
av_free ( base64 ) ;
}
avio_printf ( out , " </manifest> \n " ) ;
avio_flush ( out ) ;
avio_close ( out ) ;
return ff_rename ( temp_filename , filename , s ) ;
}
static void update_size ( AVIOContext * out , int64_t pos )
{
int64_t end = avio_tell ( out ) ;
avio_seek ( out , pos , SEEK_SET ) ;
avio_wb32 ( out , end - pos ) ;
avio_seek ( out , end , SEEK_SET ) ;
}
/* Note, the .abst files need to be served with the "binary/octet"
* mime type , otherwise at least the OSMF player can easily fail
* with " stream not found " when polling for the next fragment . */
static int write_abst ( AVFormatContext * s , OutputStream * os , int final )
{
HDSContext * c = s - > priv_data ;
AVIOContext * out ;
char filename [ 1024 ] , temp_filename [ 1024 ] ;
int i , ret ;
int64_t asrt_pos , afrt_pos ;
int start = 0 , fragments ;
int index = s - > streams [ os - > first_stream ] - > id ;
int64_t cur_media_time = 0 ;
if ( c - > window_size )
start = FFMAX ( os - > nb_fragments - c - > window_size , 0 ) ;
fragments = os - > nb_fragments - start ;
if ( final )
cur_media_time = os - > last_ts ;
else if ( os - > nb_fragments )
cur_media_time = os - > fragments [ os - > nb_fragments - 1 ] - > start_time ;
snprintf ( filename , sizeof ( filename ) ,
" %s/stream%d.abst " , s - > filename , index ) ;
snprintf ( temp_filename , sizeof ( temp_filename ) ,
" %s/stream%d.abst.tmp " , s - > filename , index ) ;
ret = avio_open2 ( & out , temp_filename , AVIO_FLAG_WRITE ,
& s - > interrupt_callback , NULL ) ;
if ( ret < 0 ) {
av_log ( s , AV_LOG_ERROR , " Unable to open %s for writing \n " , temp_filename ) ;
return ret ;
}
avio_wb32 ( out , 0 ) ; // abst size
avio_wl32 ( out , MKTAG ( ' a ' , ' b ' , ' s ' , ' t ' ) ) ;
avio_wb32 ( out , 0 ) ; // version + flags
avio_wb32 ( out , os - > fragment_index - 1 ) ; // BootstrapinfoVersion
avio_w8 ( out , final ? 0 : 0x20 ) ; // profile, live, update
avio_wb32 ( out , 1000 ) ; // timescale
avio_wb64 ( out , cur_media_time ) ;
avio_wb64 ( out , 0 ) ; // SmpteTimeCodeOffset
avio_w8 ( out , 0 ) ; // MovieIdentifer (null string)
avio_w8 ( out , 0 ) ; // ServerEntryCount
avio_w8 ( out , 0 ) ; // QualityEntryCount
avio_w8 ( out , 0 ) ; // DrmData (null string)
avio_w8 ( out , 0 ) ; // MetaData (null string)
avio_w8 ( out , 1 ) ; // SegmentRunTableCount
asrt_pos = avio_tell ( out ) ;
avio_wb32 ( out , 0 ) ; // asrt size
avio_wl32 ( out , MKTAG ( ' a ' , ' s ' , ' r ' , ' t ' ) ) ;
avio_wb32 ( out , 0 ) ; // version + flags
avio_w8 ( out , 0 ) ; // QualityEntryCount
avio_wb32 ( out , 1 ) ; // SegmentRunEntryCount
avio_wb32 ( out , 1 ) ; // FirstSegment
avio_wb32 ( out , final ? ( os - > fragment_index - 1 ) : 0xffffffff ) ; // FragmentsPerSegment
update_size ( out , asrt_pos ) ;
avio_w8 ( out , 1 ) ; // FragmentRunTableCount
afrt_pos = avio_tell ( out ) ;
avio_wb32 ( out , 0 ) ; // afrt size
avio_wl32 ( out , MKTAG ( ' a ' , ' f ' , ' r ' , ' t ' ) ) ;
avio_wb32 ( out , 0 ) ; // version + flags
avio_wb32 ( out , 1000 ) ; // timescale
avio_w8 ( out , 0 ) ; // QualityEntryCount
avio_wb32 ( out , fragments ) ; // FragmentRunEntryCount
for ( i = start ; i < os - > nb_fragments ; i + + ) {
avio_wb32 ( out , os - > fragments [ i ] - > n ) ;
avio_wb64 ( out , os - > fragments [ i ] - > start_time ) ;
avio_wb32 ( out , os - > fragments [ i ] - > duration ) ;
}
update_size ( out , afrt_pos ) ;
update_size ( out , 0 ) ;
avio_close ( out ) ;
return ff_rename ( temp_filename , filename , s ) ;
}
static int init_file ( AVFormatContext * s , OutputStream * os , int64_t start_ts )
{
int ret , i ;
ret = avio_open2 ( & os - > out , os - > temp_filename , AVIO_FLAG_WRITE ,
& s - > interrupt_callback , NULL ) ;
if ( ret < 0 )
return ret ;
avio_wb32 ( os - > out , 0 ) ;
avio_wl32 ( os - > out , MKTAG ( ' m ' , ' d ' , ' a ' , ' t ' ) ) ;
for ( i = 0 ; i < os - > nb_extra_packets ; i + + ) {
AV_WB24 ( os - > extra_packets [ i ] + 4 , start_ts ) ;
os - > extra_packets [ i ] [ 7 ] = ( start_ts > > 24 ) & 0x7f ;
avio_write ( os - > out , os - > extra_packets [ i ] , os - > extra_packet_sizes [ i ] ) ;
}
return 0 ;
}
static void close_file ( OutputStream * os )
{
int64_t pos = avio_tell ( os - > out ) ;
avio_seek ( os - > out , 0 , SEEK_SET ) ;
avio_wb32 ( os - > out , pos ) ;
avio_flush ( os - > out ) ;
avio_closep ( & os - > out ) ;
}
static int hds_write_header ( AVFormatContext * s )
{
HDSContext * c = s - > priv_data ;
int ret = 0 , i ;
AVOutputFormat * oformat ;
if ( mkdir ( s - > filename , 0777 ) = = - 1 & & errno ! = EEXIST ) {
ret = AVERROR ( errno ) ;
av_log ( s , AV_LOG_ERROR , " Failed to create directory %s \n " , s - > filename ) ;
goto fail ;
}
oformat = av_guess_format ( " flv " , NULL , NULL ) ;
if ( ! oformat ) {
ret = AVERROR_MUXER_NOT_FOUND ;
goto fail ;
}
c - > streams = av_mallocz_array ( s - > nb_streams , sizeof ( * c - > streams ) ) ;
if ( ! c - > streams ) {
ret = AVERROR ( ENOMEM ) ;
goto fail ;
}
for ( i = 0 ; i < s - > nb_streams ; i + + ) {
OutputStream * os = & c - > streams [ c - > nb_streams ] ;
AVFormatContext * ctx ;
AVStream * st = s - > streams [ i ] ;
if ( ! st - > codec - > bit_rate ) {
av_log ( s , AV_LOG_ERROR , " No bit rate set for stream %d \n " , i ) ;
ret = AVERROR ( EINVAL ) ;
goto fail ;
}
if ( st - > codec - > codec_type = = AVMEDIA_TYPE_VIDEO ) {
if ( os - > has_video ) {
c - > nb_streams + + ;
os + + ;
}
os - > has_video = 1 ;
} else if ( st - > codec - > codec_type = = AVMEDIA_TYPE_AUDIO ) {
if ( os - > has_audio ) {
c - > nb_streams + + ;
os + + ;
}
os - > has_audio = 1 ;
} else {
av_log ( s , AV_LOG_ERROR , " Unsupported stream type in stream %d \n " , i ) ;
ret = AVERROR ( EINVAL ) ;
goto fail ;
}
os - > bitrate + = s - > streams [ i ] - > codec - > bit_rate ;
if ( ! os - > ctx ) {
os - > first_stream = i ;
ctx = avformat_alloc_context ( ) ;
if ( ! ctx ) {
ret = AVERROR ( ENOMEM ) ;
goto fail ;
}
os - > ctx = ctx ;
ctx - > oformat = oformat ;
ctx - > interrupt_callback = s - > interrupt_callback ;
ctx - > pb = avio_alloc_context ( os - > iobuf , sizeof ( os - > iobuf ) ,
AVIO_FLAG_WRITE , os ,
NULL , hds_write , NULL ) ;
if ( ! ctx - > pb ) {
ret = AVERROR ( ENOMEM ) ;
goto fail ;
}
} else {
ctx = os - > ctx ;
}
s - > streams [ i ] - > id = c - > nb_streams ;
if ( ! ( st = avformat_new_stream ( ctx , NULL ) ) ) {
ret = AVERROR ( ENOMEM ) ;
goto fail ;
}
avcodec_copy_context ( st - > codec , s - > streams [ i ] - > codec ) ;
st - > codec - > codec_tag = 0 ;
st - > sample_aspect_ratio = s - > streams [ i ] - > sample_aspect_ratio ;
st - > time_base = s - > streams [ i ] - > time_base ;
}
if ( c - > streams [ c - > nb_streams ] . ctx )
c - > nb_streams + + ;
for ( i = 0 ; i < c - > nb_streams ; i + + ) {
OutputStream * os = & c - > streams [ i ] ;
int j ;
if ( ( ret = avformat_write_header ( os - > ctx , NULL ) ) < 0 ) {
goto fail ;
}
os - > ctx_inited = 1 ;
avio_flush ( os - > ctx - > pb ) ;
for ( j = 0 ; j < os - > ctx - > nb_streams ; j + + )
s - > streams [ os - > first_stream + j ] - > time_base = os - > ctx - > streams [ j ] - > time_base ;
snprintf ( os - > temp_filename , sizeof ( os - > temp_filename ) ,
" %s/stream%d_temp " , s - > filename , i ) ;
ret = init_file ( s , os , 0 ) ;
if ( ret < 0 )
goto fail ;
if ( ! os - > has_video & & c - > min_frag_duration < = 0 ) {
av_log ( s , AV_LOG_WARNING ,
" No video stream in output stream %d and no min frag duration set \n " , i ) ;
ret = AVERROR ( EINVAL ) ;
}
os - > fragment_index = 1 ;
write_abst ( s , os , 0 ) ;
}
ret = write_manifest ( s , 0 ) ;
fail :
if ( ret )
hds_free ( s ) ;
return ret ;
}
static int add_fragment ( OutputStream * os , const char * file ,
int64_t start_time , int64_t duration )
{
Fragment * frag ;
if ( duration = = 0 )
duration = 1 ;
if ( os - > nb_fragments > = os - > fragments_size ) {
int ret ;
os - > fragments_size = ( os - > fragments_size + 1 ) * 2 ;
if ( ( ret = av_reallocp_array ( & os - > fragments , os - > fragments_size ,
sizeof ( * os - > fragments ) ) ) < 0 ) {
os - > fragments_size = 0 ;
os - > nb_fragments = 0 ;
return ret ;
}
}
frag = av_mallocz ( sizeof ( * frag ) ) ;
if ( ! frag )
return AVERROR ( ENOMEM ) ;
av_strlcpy ( frag - > file , file , sizeof ( frag - > file ) ) ;
frag - > start_time = start_time ;
frag - > duration = duration ;
frag - > n = os - > fragment_index ;
os - > fragments [ os - > nb_fragments + + ] = frag ;
os - > fragment_index + + ;
return 0 ;
}
static int hds_flush ( AVFormatContext * s , OutputStream * os , int final ,
int64_t end_ts )
{
HDSContext * c = s - > priv_data ;
int i , ret = 0 ;
char target_filename [ 1024 ] ;
int index = s - > streams [ os - > first_stream ] - > id ;
if ( ! os - > packets_written )
return 0 ;
avio_flush ( os - > ctx - > pb ) ;
os - > packets_written = 0 ;
close_file ( os ) ;
snprintf ( target_filename , sizeof ( target_filename ) ,
" %s/stream%dSeg1-Frag%d " , s - > filename , index , os - > fragment_index ) ;
ret = ff_rename ( os - > temp_filename , target_filename , s ) ;
if ( ret < 0 )
return ret ;
add_fragment ( os , target_filename , os - > frag_start_ts , end_ts - os - > frag_start_ts ) ;
if ( ! final ) {
ret = init_file ( s , os , end_ts ) ;
if ( ret < 0 )
return ret ;
}
if ( c - > window_size | | ( final & & c - > remove_at_exit ) ) {
int remove = os - > nb_fragments - c - > window_size - c - > extra_window_size ;
if ( final & & c - > remove_at_exit )
remove = os - > nb_fragments ;
if ( remove > 0 ) {
for ( i = 0 ; i < remove ; i + + ) {
unlink ( os - > fragments [ i ] - > file ) ;
av_freep ( & os - > fragments [ i ] ) ;
}
os - > nb_fragments - = remove ;
memmove ( os - > fragments , os - > fragments + remove ,
os - > nb_fragments * sizeof ( * os - > fragments ) ) ;
}
}
if ( ret > = 0 )
ret = write_abst ( s , os , final ) ;
return ret ;
}
static int hds_write_packet ( AVFormatContext * s , AVPacket * pkt )
{
HDSContext * c = s - > priv_data ;
AVStream * st = s - > streams [ pkt - > stream_index ] ;
OutputStream * os = & c - > streams [ s - > streams [ pkt - > stream_index ] - > id ] ;
int64_t end_dts = os - > fragment_index * ( int64_t ) c - > min_frag_duration ;
int ret ;
if ( st - > first_dts = = AV_NOPTS_VALUE )
st - > first_dts = pkt - > dts ;
if ( ( ! os - > has_video | | st - > codec - > codec_type = = AVMEDIA_TYPE_VIDEO ) & &
av_compare_ts ( pkt - > dts - st - > first_dts , st - > time_base ,
end_dts , AV_TIME_BASE_Q ) > = 0 & &
pkt - > flags & AV_PKT_FLAG_KEY & & os - > packets_written ) {
if ( ( ret = hds_flush ( s , os , 0 , pkt - > dts ) ) < 0 )
return ret ;
}
// Note, these fragment start timestamps, that represent a whole
// OutputStream, assume all streams in it have the same time base.
if ( ! os - > packets_written )
os - > frag_start_ts = pkt - > dts ;
os - > last_ts = pkt - > dts ;
os - > packets_written + + ;
return ff_write_chained ( os - > ctx , pkt - > stream_index - os - > first_stream , pkt , s , 0 ) ;
}
static int hds_write_trailer ( AVFormatContext * s )
{
HDSContext * c = s - > priv_data ;
int i ;
for ( i = 0 ; i < c - > nb_streams ; i + + )
hds_flush ( s , & c - > streams [ i ] , 1 , c - > streams [ i ] . last_ts ) ;
write_manifest ( s , 1 ) ;
if ( c - > remove_at_exit ) {
char filename [ 1024 ] ;
snprintf ( filename , sizeof ( filename ) , " %s/index.f4m " , s - > filename ) ;
unlink ( filename ) ;
for ( i = 0 ; i < c - > nb_streams ; i + + ) {
snprintf ( filename , sizeof ( filename ) , " %s/stream%d.abst " , s - > filename , i ) ;
unlink ( filename ) ;
}
rmdir ( s - > filename ) ;
}
hds_free ( s ) ;
return 0 ;
}
# define OFFSET(x) offsetof(HDSContext, x)
# define E AV_OPT_FLAG_ENCODING_PARAM
static const AVOption options [ ] = {
{ " window_size " , " number of fragments kept in the manifest " , OFFSET ( window_size ) , AV_OPT_TYPE_INT , { . i64 = 0 } , 0 , INT_MAX , E } ,
{ " extra_window_size " , " number of fragments kept outside of the manifest before removing from disk " , OFFSET ( extra_window_size ) , AV_OPT_TYPE_INT , { . i64 = 5 } , 0 , INT_MAX , E } ,
{ " min_frag_duration " , " minimum fragment duration (in microseconds) " , OFFSET ( min_frag_duration ) , AV_OPT_TYPE_INT64 , { . i64 = 10000000 } , 0 , INT_MAX , E } ,
{ " remove_at_exit " , " remove all fragments when finished " , OFFSET ( remove_at_exit ) , AV_OPT_TYPE_INT , { . i64 = 0 } , 0 , 1 , E } ,
{ NULL } ,
} ;
static const AVClass hds_class = {
. class_name = " HDS muxer " ,
. item_name = av_default_item_name ,
. option = options ,
. version = LIBAVUTIL_VERSION_INT ,
} ;
AVOutputFormat ff_hds_muxer = {
. name = " hds " ,
. long_name = NULL_IF_CONFIG_SMALL ( " HDS Muxer " ) ,
. priv_data_size = sizeof ( HDSContext ) ,
. audio_codec = AV_CODEC_ID_AAC ,
. video_codec = AV_CODEC_ID_H264 ,
. flags = AVFMT_GLOBALHEADER | AVFMT_NOFILE ,
. write_header = hds_write_header ,
. write_packet = hds_write_packet ,
. write_trailer = hds_write_trailer ,
. priv_class = & hds_class ,
} ;